mirror of
https://github.com/mkdocs/mkdocs.git
synced 2026-03-27 09:58:31 +07:00
@@ -66,6 +66,7 @@ authors should review how [search and themes] interact.
|
||||
|
||||
### Other Changes and Additions to Development Version
|
||||
|
||||
* Add MkDocs version check to gh-deploy script (#640).
|
||||
* Improve Markdown extension error messages. (#782).
|
||||
* Drop official support for Python 3.3 and set `tornado>=5.0` (#1427).
|
||||
* Add support for GitLab edit links (#1435).
|
||||
|
||||
@@ -89,6 +89,7 @@ remote_branch_help = ("The remote branch to commit to for Github Pages. This "
|
||||
remote_name_help = ("The remote name to commit to for Github Pages. This "
|
||||
"overrides the value specified in config")
|
||||
force_help = "Force the push to the repository."
|
||||
ignore_version_help = "Ignore check that build is not being deployed with an older version of MkDocs."
|
||||
|
||||
pgk_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
@@ -172,8 +173,9 @@ def build_command(clean, config_file, strict, theme, theme_dir, site_dir):
|
||||
@click.option('-b', '--remote-branch', help=remote_branch_help)
|
||||
@click.option('-r', '--remote-name', help=remote_name_help)
|
||||
@click.option('--force', is_flag=True, help=force_help)
|
||||
@click.option('--ignore-version', is_flag=True, help=ignore_version_help)
|
||||
@common_options
|
||||
def gh_deploy_command(config_file, clean, message, remote_branch, remote_name, force):
|
||||
def gh_deploy_command(config_file, clean, message, remote_branch, remote_name, force, ignore_version):
|
||||
"""Deploy your documentation to GitHub Pages"""
|
||||
try:
|
||||
cfg = config.load_config(
|
||||
@@ -182,7 +184,7 @@ def gh_deploy_command(config_file, clean, message, remote_branch, remote_name, f
|
||||
remote_name=remote_name
|
||||
)
|
||||
build.build(cfg, dirty=not clean)
|
||||
gh_deploy.gh_deploy(cfg, message=message, force=force)
|
||||
gh_deploy.gh_deploy(cfg, message=message, force=force, ignore_version=ignore_version)
|
||||
except exceptions.ConfigurationError as e: # pragma: no cover
|
||||
# Avoid ugly, unhelpful traceback
|
||||
raise SystemExit('\n' + str(e))
|
||||
|
||||
@@ -2,6 +2,8 @@ from __future__ import unicode_literals
|
||||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
from pkg_resources import parse_version
|
||||
|
||||
import mkdocs
|
||||
from mkdocs.utils import ghp_import
|
||||
@@ -49,20 +51,49 @@ def _get_remote_url(remote_name):
|
||||
return host, path
|
||||
|
||||
|
||||
def gh_deploy(config, message=None, force=False):
|
||||
def _check_version(branch):
|
||||
|
||||
proc = subprocess.Popen(['git', 'show', '-s', '--format=%s', 'refs/heads/{}'.format(branch)],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, _ = proc.communicate()
|
||||
msg = stdout.decode('utf-8').strip()
|
||||
m = re.search(r'\d+(\.\d+)+', msg, re.X | re.I)
|
||||
previousv = parse_version(m.group()) if m else None
|
||||
currentv = parse_version(mkdocs.__version__)
|
||||
if not previousv:
|
||||
log.warn('Version check skipped: No version specificed in previous deployment.')
|
||||
elif currentv > previousv:
|
||||
log.info(
|
||||
'Previous deployment was done with MkDocs version {}; '
|
||||
'you are deploying with a newer version ({})'.format(previousv, currentv)
|
||||
)
|
||||
elif currentv < previousv:
|
||||
log.error(
|
||||
'Deployment terminated: Previous deployment was made with MkDocs version {}; '
|
||||
'you are attempting to deploy with an older version ({}). Use --ignore-version '
|
||||
'to deploy anyway.'.format(previousv, currentv)
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def gh_deploy(config, message=None, force=False, ignore_version=False):
|
||||
|
||||
if not _is_cwd_git_repo():
|
||||
log.error('Cannot deploy - this directory does not appear to be a git '
|
||||
'repository')
|
||||
|
||||
remote_branch = config['remote_branch']
|
||||
remote_name = config['remote_name']
|
||||
|
||||
if not ignore_version:
|
||||
_check_version(remote_branch)
|
||||
|
||||
if message is None:
|
||||
message = default_message
|
||||
sha = _get_current_sha(os.path.dirname(config.config_file_path))
|
||||
message = message.format(version=mkdocs.__version__, sha=sha)
|
||||
|
||||
remote_branch = config['remote_branch']
|
||||
remote_name = config['remote_name']
|
||||
|
||||
log.info("Copying '%s' to '%s' branch and pushing to GitHub.",
|
||||
config['site_dir'], config['remote_branch'])
|
||||
|
||||
|
||||
@@ -2,9 +2,14 @@ from __future__ import unicode_literals
|
||||
import textwrap
|
||||
import markdown
|
||||
import os
|
||||
import logging
|
||||
import collections
|
||||
import unittest
|
||||
|
||||
|
||||
from mkdocs import toc
|
||||
from mkdocs import config
|
||||
from mkdocs import utils
|
||||
|
||||
|
||||
def dedent(text):
|
||||
@@ -37,3 +42,97 @@ def load_config(**cfg):
|
||||
errors_warnings = conf.validate()
|
||||
assert(errors_warnings == ([], [])), errors_warnings
|
||||
return conf
|
||||
|
||||
|
||||
# Backport unittest.TestCase.assertLogs for Python 2.7
|
||||
# see https://github.com/python/cpython/blob/3.6/Lib/unittest/case.py
|
||||
|
||||
if not utils.PY3:
|
||||
_LoggingWatcher = collections.namedtuple("_LoggingWatcher",
|
||||
["records", "output"])
|
||||
|
||||
class _CapturingHandler(logging.Handler):
|
||||
"""
|
||||
A logging handler capturing all (raw and formatted) logging output.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
logging.Handler.__init__(self)
|
||||
self.watcher = _LoggingWatcher([], [])
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def emit(self, record):
|
||||
self.watcher.records.append(record)
|
||||
msg = self.format(record)
|
||||
self.watcher.output.append(msg)
|
||||
|
||||
class _AssertLogsContext(object):
|
||||
"""A context manager used to implement TestCase.assertLogs()."""
|
||||
|
||||
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
|
||||
|
||||
def __init__(self, test_case, logger_name, level):
|
||||
self.test_case = test_case
|
||||
self.logger_name = logger_name
|
||||
if level:
|
||||
self.level = logging._levelNames.get(level, level)
|
||||
else:
|
||||
self.level = logging.INFO
|
||||
self.msg = None
|
||||
|
||||
def __enter__(self):
|
||||
if isinstance(self.logger_name, logging.Logger):
|
||||
logger = self.logger = self.logger_name
|
||||
else:
|
||||
logger = self.logger = logging.getLogger(self.logger_name)
|
||||
formatter = logging.Formatter(self.LOGGING_FORMAT)
|
||||
handler = _CapturingHandler()
|
||||
handler.setFormatter(formatter)
|
||||
self.watcher = handler.watcher
|
||||
self.old_handlers = logger.handlers[:]
|
||||
self.old_level = logger.level
|
||||
self.old_propagate = logger.propagate
|
||||
logger.handlers = [handler]
|
||||
logger.setLevel(self.level)
|
||||
logger.propagate = False
|
||||
return handler.watcher
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
self.logger.handlers = self.old_handlers
|
||||
self.logger.propagate = self.old_propagate
|
||||
self.logger.setLevel(self.old_level)
|
||||
if exc_type is not None:
|
||||
# let unexpected exceptions pass through
|
||||
return False
|
||||
if len(self.watcher.records) == 0:
|
||||
self._raiseFailure(
|
||||
"no logs of level {} or higher triggered on {}"
|
||||
.format(logging.getLevelName(self.level), self.logger.name))
|
||||
|
||||
def _raiseFailure(self, standardMsg):
|
||||
msg = self.test_case._formatMessage(self.msg, standardMsg)
|
||||
raise self.test_case.failureException(msg)
|
||||
|
||||
class LogTestCase(unittest.TestCase):
|
||||
def assertLogs(self, logger=None, level=None):
|
||||
"""Fail unless a log message of level *level* or higher is emitted
|
||||
on *logger_name* or its children. If omitted, *level* defaults to
|
||||
INFO and *logger* defaults to the root logger.
|
||||
This method must be used as a context manager, and will yield
|
||||
a recording object with two attributes: `output` and `records`.
|
||||
At the end of the context manager, the `output` attribute will
|
||||
be a list of the matching formatted log messages and the
|
||||
`records` attribute will be a list of the corresponding LogRecord
|
||||
objects.
|
||||
Example::
|
||||
with self.assertLogs('foo', level='INFO') as cm:
|
||||
logging.getLogger('foo').info('first message')
|
||||
logging.getLogger('foo.bar').error('second message')
|
||||
self.assertEqual(cm.output, ['INFO:foo:first message',
|
||||
'ERROR:foo.bar:second message'])
|
||||
"""
|
||||
return _AssertLogsContext(self, logger, level)
|
||||
else:
|
||||
LogTestCase = unittest.TestCase
|
||||
|
||||
@@ -346,6 +346,8 @@ class CLITests(unittest.TestCase):
|
||||
self.assertEqual(g_kwargs['message'], None)
|
||||
self.assertTrue('force' in g_kwargs)
|
||||
self.assertEqual(g_kwargs['force'], False)
|
||||
self.assertTrue('ignore_version' in g_kwargs)
|
||||
self.assertEqual(g_kwargs['ignore_version'], False)
|
||||
self.assertEqual(mock_build.call_count, 1)
|
||||
b_args, b_kwargs = mock_build.call_args
|
||||
self.assertTrue('dirty' in b_kwargs)
|
||||
@@ -471,3 +473,19 @@ class CLITests(unittest.TestCase):
|
||||
self.assertEqual(g_kwargs['force'], True)
|
||||
self.assertEqual(mock_build.call_count, 1)
|
||||
self.assertEqual(mock_load_config.call_count, 1)
|
||||
|
||||
@mock.patch('mkdocs.config.load_config', autospec=True)
|
||||
@mock.patch('mkdocs.commands.build.build', autospec=True)
|
||||
@mock.patch('mkdocs.commands.gh_deploy.gh_deploy', autospec=True)
|
||||
def test_gh_deploy_ognore_version(self, mock_gh_deploy, mock_build, mock_load_config):
|
||||
|
||||
result = self.runner.invoke(
|
||||
cli.cli, ['gh-deploy', '--ignore-version'], catch_exceptions=False)
|
||||
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
self.assertEqual(mock_gh_deploy.call_count, 1)
|
||||
g_args, g_kwargs = mock_gh_deploy.call_args
|
||||
self.assertTrue('ignore_version' in g_kwargs)
|
||||
self.assertEqual(g_kwargs['ignore_version'], True)
|
||||
self.assertEqual(mock_build.call_count, 1)
|
||||
self.assertEqual(mock_load_config.call_count, 1)
|
||||
|
||||
@@ -3,8 +3,9 @@ from __future__ import unicode_literals
|
||||
import unittest
|
||||
import mock
|
||||
|
||||
from mkdocs.tests.base import load_config
|
||||
from mkdocs.tests.base import load_config, LogTestCase
|
||||
from mkdocs.commands import gh_deploy
|
||||
from mkdocs import __version__
|
||||
|
||||
|
||||
class TestGitHubDeploy(unittest.TestCase):
|
||||
@@ -99,6 +100,32 @@ class TestGitHubDeploy(unittest.TestCase):
|
||||
)
|
||||
gh_deploy.gh_deploy(config)
|
||||
|
||||
@mock.patch('mkdocs.commands.gh_deploy._is_cwd_git_repo', return_value=True)
|
||||
@mock.patch('mkdocs.commands.gh_deploy._get_current_sha', return_value='shashas')
|
||||
@mock.patch('mkdocs.commands.gh_deploy._get_remote_url', return_value=(None, None))
|
||||
@mock.patch('mkdocs.commands.gh_deploy._check_version')
|
||||
@mock.patch('mkdocs.commands.gh_deploy.ghp_import.ghp_import', return_value=(True, ''))
|
||||
def test_deploy_ignore_version_default(self, mock_import, check_version, get_remote, get_sha, is_repo):
|
||||
|
||||
config = load_config(
|
||||
remote_branch='test',
|
||||
)
|
||||
gh_deploy.gh_deploy(config)
|
||||
check_version.assert_called_once()
|
||||
|
||||
@mock.patch('mkdocs.commands.gh_deploy._is_cwd_git_repo', return_value=True)
|
||||
@mock.patch('mkdocs.commands.gh_deploy._get_current_sha', return_value='shashas')
|
||||
@mock.patch('mkdocs.commands.gh_deploy._get_remote_url', return_value=(None, None))
|
||||
@mock.patch('mkdocs.commands.gh_deploy._check_version')
|
||||
@mock.patch('mkdocs.commands.gh_deploy.ghp_import.ghp_import', return_value=(True, ''))
|
||||
def test_deploy_ignore_version(self, mock_import, check_version, get_remote, get_sha, is_repo):
|
||||
|
||||
config = load_config(
|
||||
remote_branch='test',
|
||||
)
|
||||
gh_deploy.gh_deploy(config, ignore_version=True)
|
||||
check_version.assert_not_called()
|
||||
|
||||
@mock.patch('mkdocs.utils.ghp_import.ghp_import')
|
||||
@mock.patch('mkdocs.commands.gh_deploy.log')
|
||||
def test_deploy_error(self, mock_log, mock_import):
|
||||
@@ -112,3 +139,43 @@ class TestGitHubDeploy(unittest.TestCase):
|
||||
self.assertRaises(SystemExit, gh_deploy.gh_deploy, config)
|
||||
mock_log.error.assert_called_once_with('Failed to deploy to GitHub with error: \n%s',
|
||||
error_string)
|
||||
|
||||
|
||||
class TestGitHubDeployLogs(LogTestCase):
|
||||
|
||||
@mock.patch('subprocess.Popen')
|
||||
def test_mkdocs_newer(self, mock_popeno):
|
||||
|
||||
mock_popeno().communicate.return_value = (b'Deployed 12345678 with MkDocs version: 0.1.2\n', b'')
|
||||
|
||||
with self.assertLogs('mkdocs', level='INFO') as cm:
|
||||
gh_deploy._check_version('gh-pages')
|
||||
self.assertEqual(
|
||||
cm.output, ['INFO:mkdocs.commands.gh_deploy:Previous deployment was done with MkDocs '
|
||||
'version 0.1.2; you are deploying with a newer version ({})'.format(__version__)]
|
||||
)
|
||||
|
||||
@mock.patch('subprocess.Popen')
|
||||
def test_mkdocs_older(self, mock_popeno):
|
||||
|
||||
mock_popeno().communicate.return_value = (b'Deployed 12345678 with MkDocs version: 10.1.2\n', b'')
|
||||
|
||||
with self.assertLogs('mkdocs', level='ERROR') as cm:
|
||||
self.assertRaises(SystemExit, gh_deploy._check_version, 'gh-pages')
|
||||
self.assertEqual(
|
||||
cm.output, ['ERROR:mkdocs.commands.gh_deploy:Deployment terminated: Previous deployment was made with '
|
||||
'MkDocs version 10.1.2; you are attempting to deploy with an older version ({}). Use '
|
||||
'--ignore-version to deploy anyway.'.format(__version__)]
|
||||
)
|
||||
|
||||
@mock.patch('subprocess.Popen')
|
||||
def test_version_unknown(self, mock_popeno):
|
||||
|
||||
mock_popeno().communicate.return_value = (b'No version specified\n', b'')
|
||||
|
||||
with self.assertLogs('mkdocs', level='WARNING') as cm:
|
||||
gh_deploy._check_version('gh-pages')
|
||||
self.assertEqual(
|
||||
cm.output,
|
||||
['WARNING:mkdocs.commands.gh_deploy:Version check skipped: No version specificed in previous deployment.']
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user