diff --git a/CHANGES.md b/CHANGES.md index 1d5c0cb..7674090 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### New features +- Add support for applying arbitrary properties to documentation versions - Deploy aliases using symbolic links by default; this can be configured via `--alias-type` on the command line or `alias_type` in the `mike` MkDocs plugin - Avoid creating empty commits by default; if you want empty commits, pass diff --git a/README.md b/README.md index 6970e61..7e9e2cf 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,10 @@ redirect template with `-T`/`--template`; this takes a path to a [Jinja][jinja] template that accepts an `{{href}}` variable. If you'd like to specify a title for this version that doesn't match the version -string, you can pass `-t TITLE`/`--title=TITLE` as well. +string, you can pass `-t TITLE`/`--title=TITLE` as well. You can set custom +properties for this version as well, using `--prop-set`, `--prop-set-string`, +`--prop-set-all`, `--prop-delete`, and `--prop-delete-all` (see the [Managing +Properties][#managing-properties] section for more details). In addition, you can specify where to deploy your docs via `-b`/`--branch`, `-r`/`--remote`, and `--deploy-prefix`, specifying the branch, remote, and @@ -240,6 +243,41 @@ existing alias points to. Once again, you can specify `--branch`, `--push`, etc to control how the commit is handled. +### Managing Properties + +Each version of your documentation can have any arbitrary properties assigned to +it that you like. You can use these properties to hold extra metadata, and then +your documentation theme can consult those properties to do whatever you like. +You can get properties via `props` command: + +```sh +mike props [identifier] [prop] +``` + +If `prop` is specified, this will return the value of that property; otherwise, +it will return all of that version's properties as a JSON object. + +You can also set properties by specifying one or more of `--set prop=json`, +`--set-string prop=str`, `--set-all json`, `--delete prop`, and `--delete-all`. +(If you prefer, you can also set properties at the same time as deploying via +the `--prop-*` options.) + +When getting or setting a particular property, you can specify it with a +limited JSONPath-like syntax. You can use bare field names, quoted field +names, and indices/field names inside square brackets. The only operator +supported is `.`. For example, this is a valid expression: + +```javascript +foo."bar"[0].["baz"] +``` + +When setting values, you can add to the head or tail of a list via the `head` +or `tail` keywords, e.g.: + +```javascript +foo[head] +``` + ### More Details For more details on the available options, consult the `--help` command for @@ -314,10 +352,15 @@ this: ```js [ {"version": "1.0", "title": "1.0.1", "aliases": ["latest"]}, - {"version": "0.9", "title": "0.9", "aliases": []} + {"version": "0.9", "title": "0.9", "aliases": [], properties: "anything"} ] ``` +Every version has a `version` string, a `title` (which may be the same as +`version`), a list of `aliases`, and optionally, a `properties` attribute that +can hold anything at all. These properties can be used by other packages, +themes, etc in order to add their own custom metadata to each version. + If you're creating a third-party extension to an existing theme, you add a setuptools entry point for `mike.themes` pointing to a Python submodule that contains `css/` and `js/` subdirectories containing the extra code to be diff --git a/mike/arguments.py b/mike/arguments.py index 9482f9a..575266e 100644 --- a/mike/arguments.py +++ b/mike/arguments.py @@ -1,4 +1,6 @@ from argparse import * +import re as _re +import textwrap as _textwrap _ArgumentParser = ArgumentParser _Action = Action @@ -13,6 +15,13 @@ def _add_complete(argument, complete): return argument +class ParagraphDescriptionHelpFormatter(HelpFormatter): + def _fill_text(self, text, width, indent): + # Re-fill text, but keep paragraphs. Why isn't this the default??? + return '\n\n'.join(_textwrap.fill(i, width) for i in + _re.split('\n\n', text.strip())) + + class Action(_Action): def __init__(self, *args, complete=None, **kwargs): super().__init__(*args, **kwargs) diff --git a/mike/commands.py b/mike/commands.py index cc07035..0dbefb7 100644 --- a/mike/commands.py +++ b/mike/commands.py @@ -62,7 +62,7 @@ def make_nojekyll(): @contextmanager def deploy(cfg, version, title=None, aliases=[], update_aliases=False, alias_type=AliasType.symlink, template=None, *, branch='gh-pages', - message=None, allow_empty=False, deploy_prefix=''): + message=None, allow_empty=False, deploy_prefix='', set_props=[]): if message is None: message = ( 'Deployed {rev} to {doc_version}{deploy_prefix} with MkDocs ' + @@ -81,6 +81,9 @@ def deploy(cfg, version, title=None, aliases=[], update_aliases=False, destdir = os.path.join(deploy_prefix, version_str) alias_destdirs = [os.path.join(deploy_prefix, i) for i in info.aliases] + for path, value in set_props: + info.set_property(path, value) + # Let the caller perform the build. yield @@ -209,6 +212,43 @@ def alias(cfg, identifier, aliases, update_aliases=False, commit.add_file(versions_to_file_info(all_versions, deploy_prefix)) +def get_property(identifier, prop, *, branch='gh-pages', deploy_prefix=''): + all_versions = list_versions(branch, deploy_prefix) + try: + real_version = all_versions.find(identifier, strict=True)[0] + info = all_versions[real_version] + except KeyError as e: + raise ValueError('identifier {} does not exist'.format(e)) + + return info.get_property(prop) + + +def set_properties(identifier, set_props, *, branch='gh-pages', message=None, + allow_empty=False, deploy_prefix=''): + all_versions = list_versions(branch, deploy_prefix) + try: + real_version = all_versions.find(identifier, strict=True)[0] + info = all_versions[real_version] + except KeyError as e: + raise ValueError('identifier {} does not exist'.format(e)) + + if message is None: + message = ( + 'Set properties for {doc_version}{deploy_prefix} with mike ' + + '{mike_version}' + ).format( + doc_version=real_version, + deploy_prefix=_format_deploy_prefix(deploy_prefix), + mike_version=app_version + ) + + for path, value in set_props: + info.set_property(path, value) + + with git_utils.Commit(branch, message, allow_empty=allow_empty) as commit: + commit.add_file(versions_to_file_info(all_versions, deploy_prefix)) + + def retitle(identifier, title, *, branch='gh-pages', message=None, allow_empty=False, deploy_prefix=''): if message is None: diff --git a/mike/driver.py b/mike/driver.py index 65f948a..039ac86 100644 --- a/mike/driver.py +++ b/mike/driver.py @@ -1,9 +1,12 @@ +import json import os import sys from contextlib import contextmanager -from . import arguments, commands +from . import arguments +from . import commands from . import git_utils +from . import jsonpath from . import mkdocs_utils from .app_version import version as app_version from .mkdocs_plugin import MikePlugin @@ -32,6 +35,22 @@ the target branch. """ +props_desc = """ +Get or set properties for the specified version. + +When getting or setting a particular property, you can specify it with a +limited JSONPath-like syntax. You can use bare field names, quoted field names, +and indices/field names inside square brackets. The only operator supported is +`.`. For example, this is a valid expression: + + foo."bar"[0].["baz"] + +When setting values, you can add to the head or tail of a list via the `head` +or `tail` keywords, e.g.: + + foo[head] +""" + retitle_desc = """ Change the descriptive title of the specified version of the documentation on the target branch. @@ -80,13 +99,40 @@ def add_git_arguments(parser, *, commit=True, deploy_prefix=True): if deploy_prefix: git.add_argument('--deploy-prefix', metavar='PATH', complete='directory', - help=('subdirectory within {branch} where generated ' - 'docs should be deployed to')) + help=('subdirectory within {branch} where ' + + 'generated docs should be deployed to')) git.add_argument('--ignore-remote-status', action='store_true', help="don't check status of remote branch") +def add_set_prop_arguments(parser, *, prefix=''): + def parse_set_json(expression): + result = jsonpath.parse_set(expression) + return result[0], json.loads(result[1]) + + prop_p = parser.add_argument_group('property manipulation arguments') + prop_p.add_argument('--{}set'.format(prefix), metavar='PROP=JSON', + action='append', type=parse_set_json, dest='set_props', + help='set the property at PROP to a JSON value') + prop_p.add_argument('--{}set-string'.format(prefix), metavar='PROP=STRING', + action='append', type=jsonpath.parse_set, + dest='set_props', + help='set the property at PROP to a STRING value') + prop_p.add_argument('--{}set-all'.format(prefix), metavar='JSON', + action='append', type=lambda x: ('', json.loads(x)), + dest='set_props', + help='set all properties to a JSON value') + prop_p.add_argument('--{}delete'.format(prefix), metavar='PROP', + action='append', + type=lambda x: (jsonpath.parse(x), jsonpath.Deleted), + dest='set_props', help='delete the property at PROP') + prop_p.add_argument('--{}delete-all'.format(prefix), + action='append_const', const=('', jsonpath.Deleted), + dest='set_props', help='delete all properties') + return prop_p + + def load_mkdocs_config(args, strict=False): def maybe_set(args, cfg, field, cfg_field=None): if getattr(args, field, object()) is None: @@ -149,7 +195,8 @@ def deploy(parser, args): args.update_aliases, alias_type, args.template, branch=args.branch, message=args.message, allow_empty=args.allow_empty, - deploy_prefix=args.deploy_prefix), \ + deploy_prefix=args.deploy_prefix, + set_props=args.set_props or []), \ mkdocs_utils.inject_plugin(args.config_file) as config_file: mkdocs_utils.build(config_file, args.version) if args.push: @@ -179,6 +226,28 @@ def alias(parser, args): git_utils.push_branch(args.remote, args.branch) +def props(parser, args): + load_mkdocs_config(args) + check_remote_status(args, strict=args.set_props) + + if args.get_prop and args.set_props: + raise ValueError('cannot get and set properties at the same time') + elif args.set_props: + with handle_empty_commit(): + commands.set_properties(args.identifier, args.set_props, + branch=args.branch, message=args.message, + allow_empty=args.allow_empty, + deploy_prefix=args.deploy_prefix) + if args.push: + git_utils.push_branch(args.remote, args.branch) + else: + print(json.dumps( + commands.get_property(args.identifier, args.get_prop, + branch=args.branch, + deploy_prefix=args.deploy_prefix) + )) + + def retitle(parser, args): load_mkdocs_config(args) check_remote_status(args, strict=True) @@ -282,6 +351,7 @@ def main(): deploy_p.add_argument('-T', '--template', complete='file', help='template file to use for redirects') add_git_arguments(deploy_p) + add_set_prop_arguments(deploy_p, prefix='prop-') deploy_p.add_argument('version', metavar='VERSION', help='version to deploy this build to') deploy_p.add_argument('aliases', nargs='*', metavar='ALIAS', @@ -315,6 +385,18 @@ def main(): alias_p.add_argument('aliases', nargs='*', metavar='ALIAS', help='new alias to add') + props_p = subparsers.add_parser( + 'props', description=props_desc, help='get/set version properties', + formatter_class=arguments.ParagraphDescriptionHelpFormatter + ) + props_p.set_defaults(func=props) + add_git_arguments(props_p) + add_set_prop_arguments(props_p) + props_p.add_argument('identifier', metavar='IDENTIFIER', + help='existing version or alias') + props_p.add_argument('get_prop', nargs='?', metavar='PROP', default='', + help='property to get') + retitle_p = subparsers.add_parser( 'retitle', description=retitle_desc, help='change the title of a version' diff --git a/test/integration/test_command_line.py b/test/integration/test_command_line.py index 64fadb8..430f49f 100644 --- a/test/integration/test_command_line.py +++ b/test/integration/test_command_line.py @@ -20,6 +20,12 @@ def test_help_subcommand_extra(self): '--ignore-remote-status']) self.assertRegex(output, r'^usage: mike deploy') + def test_help_paragraph_formatter(self): + output = assertPopen(['mike', 'help', 'props']) + self.assertRegex(output, r'^usage: mike props') + self.assertRegex(output, ('(?m)^Get or set properties for the ' + + 'specified version\.\n\nWhen getting')) + class GenerateCompletionTest(unittest.TestCase): def test_completion(self): diff --git a/test/integration/test_deploy.py b/test/integration/test_deploy.py index b9ca627..6fda85e 100644 --- a/test/integration/test_deploy.py +++ b/test/integration/test_deploy.py @@ -105,6 +105,16 @@ def test_aliases_copy(self): versions.VersionInfo('1.0', aliases=['latest']) ], alias_type=AliasType.copy) + def test_props(self): + assertPopen(['mike', 'deploy', '1.0', + '--prop-set', 'foo.bar=[1,2,3]', + '--prop-set', 'foo.bar[1]=true', + '--prop-delete', 'foo.bar[0]']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_deploy(expected_versions=[ + versions.VersionInfo('1.0', properties={'foo': {'bar': [True, 3]}}) + ]) + def test_update(self): assertPopen(['mike', 'deploy', '1.0', 'latest']) assertPopen(['mike', 'deploy', '1.0', 'greatest', '-t', '1.0.1']) diff --git a/test/integration/test_props.py b/test/integration/test_props.py new file mode 100644 index 0000000..7056c84 --- /dev/null +++ b/test/integration/test_props.py @@ -0,0 +1,379 @@ +import os +import unittest + +from . import assertPopen, assertOutput +from .. import * +from mike import git_utils, versions + + +class PropsTestCase(unittest.TestCase): + def setUp(self): + self.stage = stage_dir(self.stage_dir) + git_init() + copytree(os.path.join(test_data_dir, 'basic_theme'), self.stage) + check_call_silent(['git', 'add', 'mkdocs.yml', 'docs']) + check_call_silent(['git', 'commit', '-m', 'initial commit']) + + def _add_version(self, version='1.0', aliases=[], properties=None, + branch='gh-pages', deploy_prefix=''): + all_versions = versions.Versions() + all_versions.add(version, aliases=aliases) + all_versions[version].properties = properties + + with git_utils.Commit(branch, 'commit message') as commit: + commit.add_file(git_utils.FileInfo( + os.path.join(deploy_prefix, 'versions.json'), + all_versions.dumps() + )) + + def _test_set_props(self, expected_versions=[versions.VersionInfo( + '1.0', properties={'hidden': True} + )], expected_message=None, directory='.'): + message = assertPopen(['git', 'log', '-1', '--pretty=%B']).rstrip() + if expected_message: + self.assertEqual(message, expected_message) + else: + self.assertRegex( + message, + r'^Set properties for {}( in .*)? with mike \S+$' + .format(expected_versions[0].version) + ) + + with open(os.path.join(directory, 'versions.json')) as f: + self.assertEqual(list(versions.Versions.loads(f.read())), + expected_versions) + + +class TestGetProp(PropsTestCase): + stage_dir = 'get_prop' + + def test_get_prop(self): + self._add_version(properties={'hidden': True}) + assertOutput(self, ['mike', 'props', '1.0', 'hidden'], 'true\n') + + def test_branch(self): + self._add_version(properties={'hidden': True}, branch='branch') + assertOutput(self, ['mike', 'props', '-b', 'branch', '1.0', 'hidden'], + 'true\n') + + def test_from_subdir(self): + self._add_version(properties={'hidden': True}) + os.mkdir('sub') + with pushd('sub'): + assertPopen(['mike', 'props', '1.0', 'hidden'], returncode=1) + assertOutput(self, ['mike', 'props', '1.0', 'hidden', + '-F', '../mkdocs.yml'], 'true\n') + + def test_from_subdir_explicit_branch(self): + self._add_version(properties={'hidden': True}) + os.mkdir('sub') + with pushd('sub'): + assertPopen(['mike', 'props', '1.0', 'hidden'], returncode=1) + assertOutput(self, ['mike', 'props', '1.0', 'hidden', + '-b', 'gh-pages', '-r', 'origin'], 'true\n') + + def test_deploy_prefix(self): + self._add_version(properties={'hidden': True}, deploy_prefix='prefix') + assertOutput(self, ['mike', 'props', '1.0', 'hidden', + '--deploy-prefix', 'prefix'], 'true\n') + + def test_ahead_remote(self): + self._add_version() + origin_rev = git_utils.get_latest_commit('gh-pages') + + stage_dir('get_prop_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + check_call_silent(['git', 'fetch', 'origin', 'gh-pages:gh-pages']) + git_config() + + self._add_version(properties={'hidden': True}) + clone_rev = git_utils.get_latest_commit('gh-pages') + + assertOutput(self, ['mike', 'props', '1.0', 'hidden'], 'true\n') + self.assertEqual(git_utils.get_latest_commit('gh-pages'), clone_rev) + self.assertEqual(git_utils.get_latest_commit('gh-pages^'), origin_rev) + + def test_behind_remote(self): + self._add_version() + stage_dir('get_prop_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + check_call_silent(['git', 'fetch', 'origin', 'gh-pages:gh-pages']) + git_config() + + with pushd(self.stage): + self._add_version(properties={'hidden': True}) + origin_rev = git_utils.get_latest_commit('gh-pages') + check_call_silent(['git', 'fetch', 'origin']) + + assertOutput(self, ['mike', 'props', '1.0', 'hidden'], 'true\n') + self.assertEqual(git_utils.get_latest_commit('gh-pages'), origin_rev) + + def test_diverged_remote(self): + self._add_version() + stage_dir('get_prop_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + check_call_silent(['git', 'fetch', 'origin', 'gh-pages:gh-pages']) + git_config() + + with pushd(self.stage): + self._add_version(properties={'hidden': True}) + + self._add_version(properties={'hidden': False}) + clone_rev = git_utils.get_latest_commit('gh-pages') + check_call_silent(['git', 'fetch', 'origin']) + + assertOutput(self, ['mike', 'props', '1.0', 'hidden'], + stdout='false\n', + stderr=('warning: gh-pages has diverged from ' + + 'origin/gh-pages\n')) + self.assertEqual(git_utils.get_latest_commit('gh-pages'), clone_rev) + + assertOutput(self, ['mike', 'props', '1.0', 'hidden', + '--ignore-remote-status'], + stdout='false\n', stderr='') + self.assertEqual(git_utils.get_latest_commit('gh-pages'), clone_rev) + + def test_get_and_set(self): + self._add_version(properties={'hidden': True}) + assertOutput( + self, ['mike', 'props', '1.0', 'hidden', '--set', 'dev=true'], + stdout='', stderr=('error: cannot get and set properties at the ' + + 'same time\n'), + returncode=1 + ) + + +class TestSetProps(PropsTestCase): + stage_dir = 'set_props' + + def test_set_prop(self): + self._add_version() + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props() + + def test_set_string_prop(self): + self._add_version() + assertPopen(['mike', 'props', '1.0', '--set-string', 'kind=dev']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props(expected_versions=[versions.VersionInfo( + '1.0', properties={'kind': 'dev'} + )]) + + def test_set_all_props(self): + self._add_version() + assertPopen(['mike', 'props', '1.0', '--set-all', '{"hidden": true}']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props() + + def test_delete_prop(self): + self._add_version(properties={'hidden': True, 'kind': 'dev'}) + assertPopen(['mike', 'props', '1.0', '--delete', 'kind']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props() + + def test_delete_all_props(self): + self._add_version(properties={'hidden': True, 'kind': 'dev'}) + assertPopen(['mike', 'props', '1.0', '--delete-all']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props(expected_versions=[versions.VersionInfo('1.0')]) + + def test_branch(self): + self._add_version(branch='branch') + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', + '-b', 'branch']) + check_call_silent(['git', 'checkout', 'branch']) + self._test_set_props() + + def test_from_subdir(self): + self._add_version() + os.mkdir('sub') + with pushd('sub'): + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true'], + returncode=1) + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', + '-F', '../mkdocs.yml']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props() + + def test_from_subdir_explicit_branch(self): + self._add_version() + os.mkdir('sub') + with pushd('sub'): + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', + '-b', 'gh-pages', '-r', 'origin']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props() + + def test_commit_message(self): + self._add_version() + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', + '-m', 'commit message']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props(expected_message='commit message') + + def test_deploy_prefix(self): + self._add_version(deploy_prefix='prefix') + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', + '--deploy-prefix', 'prefix']) + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_props(directory='prefix') + + def test_push(self): + self._add_version() + check_call_silent(['git', 'config', 'receive.denyCurrentBranch', + 'ignore']) + stage_dir('set_props_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + git_config() + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', '-p']) + clone_rev = git_utils.get_latest_commit('gh-pages') + + with pushd(self.stage): + origin_rev = git_utils.get_latest_commit('gh-pages') + self.assertEqual(origin_rev, clone_rev) + + def test_remote_empty(self): + stage_dir('set_props_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + git_config() + + self._add_version() + old_rev = git_utils.get_latest_commit('gh-pages') + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true']) + self.assertEqual(git_utils.get_latest_commit('gh-pages^'), old_rev) + + def test_local_empty(self): + self._add_version() + origin_rev = git_utils.get_latest_commit('gh-pages') + + stage_dir('set_props_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + git_config() + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true']) + self.assertEqual(git_utils.get_latest_commit('gh-pages^'), origin_rev) + + def test_ahead_remote(self): + self._add_version() + origin_rev = git_utils.get_latest_commit('gh-pages') + + stage_dir('set_props_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + check_call_silent(['git', 'fetch', 'origin', 'gh-pages:gh-pages']) + git_config() + + self._add_version(properties={'hidden': True}) + old_rev = git_utils.get_latest_commit('gh-pages') + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true']) + self.assertEqual(git_utils.get_latest_commit('gh-pages^'), old_rev) + self.assertEqual(git_utils.get_latest_commit('gh-pages^^'), origin_rev) + + def test_behind_remote(self): + self._add_version() + + stage_dir('set_props_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + check_call_silent(['git', 'fetch', 'origin', 'gh-pages:gh-pages']) + git_config() + + with pushd(self.stage): + self._add_version(properties={'hidden': True}) + origin_rev = git_utils.get_latest_commit('gh-pages') + check_call_silent(['git', 'fetch', 'origin']) + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true']) + self.assertEqual(git_utils.get_latest_commit('gh-pages^'), origin_rev) + + def test_diverged_remote(self): + self._add_version() + + stage_dir('set_props_clone') + check_call_silent(['git', 'clone', self.stage, '.']) + check_call_silent(['git', 'fetch', 'origin', 'gh-pages:gh-pages']) + git_config() + + with pushd(self.stage): + self._add_version(properties={'hidden': True}) + + self._add_version(properties={'hidden': False}) + clone_rev = git_utils.get_latest_commit('gh-pages') + check_call_silent(['git', 'fetch', 'origin']) + + assertOutput( + self, ['mike', 'props', '1.0', '--set', 'hidden=true'], + stdout='', stderr=( + 'error: gh-pages has diverged from origin/gh-pages\n' + + " If you're sure this is intended, retry with " + + '--ignore-remote-status\n' + ), returncode=1 + ) + self.assertEqual(git_utils.get_latest_commit('gh-pages'), clone_rev) + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', + '--ignore-remote-status']) + self.assertEqual(git_utils.get_latest_commit('gh-pages^'), clone_rev) + + +class TestSetPropsOtherRemote(PropsTestCase): + def _deploy(self, branch=None, versions=['1.0'], deploy_prefix=''): + extra_args = ['-b', branch] if branch else [] + if deploy_prefix: + extra_args.extend(['--deploy-prefix', deploy_prefix]) + for i in versions: + assertPopen(['mike', 'deploy', i] + extra_args) + + def setUp(self): + self.stage_origin = stage_dir('set_props_remote') + git_init() + copytree(os.path.join(test_data_dir, 'remote'), self.stage_origin) + check_call_silent(['git', 'add', 'mkdocs.yml', 'docs']) + check_call_silent(['git', 'commit', '-m', 'initial commit']) + check_call_silent(['git', 'config', 'receive.denyCurrentBranch', + 'ignore']) + + def _clone(self): + self.stage = stage_dir('set_props_remote_clone') + check_call_silent(['git', 'clone', self.stage_origin, '.']) + git_config() + + def _test_rev(self, branch): + clone_rev = git_utils.get_latest_commit(branch) + with pushd(self.stage_origin): + self.assertEqual(git_utils.get_latest_commit(branch), clone_rev) + + def test_default(self): + self._add_version(branch='mybranch') + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=false']) + self._clone() + check_call_silent(['git', 'remote', 'rename', 'origin', 'myremote']) + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', '-p']) + check_call_silent(['git', 'checkout', 'mybranch']) + self._test_set_props() + self._test_rev('mybranch') + + def test_explicit_branch(self): + self._add_version(branch='pages') + self._clone() + check_call_silent(['git', 'remote', 'rename', 'origin', 'myremote']) + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', '-p', + '-b', 'pages']) + check_call_silent(['git', 'checkout', 'pages']) + self._test_set_props() + self._test_rev('pages') + + def test_explicit_remote(self): + self._add_version(branch='mybranch') + self._clone() + check_call_silent(['git', 'remote', 'rename', 'origin', 'remote']) + + assertPopen(['mike', 'props', '1.0', '--set', 'hidden=true', '-p', + '-r', 'remote']) + check_call_silent(['git', 'checkout', 'mybranch']) + self._test_set_props() + self._test_rev('mybranch') diff --git a/test/unit/test_commands.py b/test/unit/test_commands.py index b677cf8..d20d200 100644 --- a/test/unit/test_commands.py +++ b/test/unit/test_commands.py @@ -1,3 +1,4 @@ +import json import os import re import shutil @@ -9,6 +10,7 @@ from .mock_server import MockRequest, MockServer from mike import commands, git_utils, versions from mike.commands import AliasType +from mike.jsonpath import Deleted def mock_config(site_dir, remote_name='origin', @@ -191,6 +193,18 @@ def test_aliases_copy(self): versions.VersionInfo('1.0', aliases=['latest']) ], alias_type=AliasType.copy) + def test_props(self): + with commands.deploy(self.cfg, '1.0', set_props=[ + (['foo', 'bar'], [1, 2, 3]), + (['foo', 'bar', 1], True), + (['foo', 'bar', 0], Deleted), + ]): + self._mock_build() + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_deploy(expected_versions=[ + versions.VersionInfo('1.0', properties={'foo': {'bar': [True, 3]}}) + ]) + def test_branch(self): with commands.deploy(self.cfg, '1.0', branch='branch'): self._mock_build() @@ -503,6 +517,146 @@ def test_alias_invalid_version(self): ['alias'], branch='branch') +class TestPropertyBase(unittest.TestCase): + def setUp(self): + self.stage = stage_dir(self.stage_dir) + git_init() + + def _commit_versions(self, *args, branch='gh-pages', deploy_prefix=''): + with git_utils.Commit(branch, 'add versions.json') as commit: + commit.add_file(git_utils.FileInfo( + os.path.join(deploy_prefix, 'versions.json'), + json.dumps([i.to_json() for i in args]) + )) + + +class TestGetProperty(TestPropertyBase): + stage_dir = 'get_property' + + def test_get_property(self): + self._commit_versions(versions.VersionInfo( + '1.0', properties={'hidden': True} + )) + self.assertEqual(commands.get_property('1.0', ''), {'hidden': True}) + self.assertEqual(commands.get_property('1.0', 'hidden'), True) + + def test_get_property_from_alias(self): + self._commit_versions(versions.VersionInfo( + '1.0', aliases=['latest'], properties={'hidden': True} + )) + self.assertEqual(commands.get_property('latest', ''), {'hidden': True}) + self.assertEqual(commands.get_property('latest', 'hidden'), True) + + def test_no_properties(self): + self._commit_versions(versions.VersionInfo('1.0')) + self.assertEqual(commands.get_property('1.0', ''), None) + + def test_branch(self): + self._commit_versions(versions.VersionInfo( + '1.0', properties={'hidden': True} + ), branch='branch') + self.assertEqual(commands.get_property('1.0', '', branch='branch'), + {'hidden': True}) + + def test_deploy_prefix(self): + self._commit_versions(versions.VersionInfo( + '1.0', properties={'hidden': True} + ), deploy_prefix='prefix') + self.assertEqual( + commands.get_property('1.0', '', deploy_prefix='prefix'), + {'hidden': True} + ) + + def test_invalid_version(self): + self._commit_versions(versions.VersionInfo('1.0')) + with self.assertRaises(ValueError): + commands.get_property('2.0', '') + + +class TestSetProperties(TestPropertyBase): + stage_dir = 'set_properties' + + def _test_set_properties(self, expected_versions, expected_message=None, + directory='.'): + message = check_output(['git', 'log', '-1', '--pretty=%B']).rstrip() + if expected_message: + self.assertEqual(message, expected_message) + else: + self.assertRegex( + message, + r'^Set properties for \S+( in .*)? with mike \S+$' + ) + + with open(os.path.join(directory, 'versions.json')) as f: + self.assertEqual(list(versions.Versions.loads(f.read())), + expected_versions) + + def test_set_property(self): + self._commit_versions(versions.VersionInfo('1.0')) + commands.set_properties('1.0', [('foo.bar', True)]) + + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_properties([ + versions.VersionInfo('1.0', properties={'foo': {'bar': True}}), + ]) + + def test_set_all_properties(self): + self._commit_versions(versions.VersionInfo( + '1.0', properties={'hidden': True} + )) + commands.set_properties('1.0', [('', 'hello')]) + + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_properties([ + versions.VersionInfo('1.0', properties='hello'), + ]) + + def test_set_property_from_alias(self): + self._commit_versions(versions.VersionInfo('1.0', aliases=['latest'])) + commands.set_properties('latest', [('foo.bar', True)]) + + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_properties([ + versions.VersionInfo('1.0', aliases=['latest'], + properties={'foo': {'bar': True}}), + ]) + + def test_branch(self): + self._commit_versions(versions.VersionInfo('1.0'), branch='branch') + commands.set_properties('1.0', [('foo.bar', True)], branch='branch') + + check_call_silent(['git', 'checkout', 'branch']) + self._test_set_properties([ + versions.VersionInfo('1.0', properties={'foo': {'bar': True}}), + ]) + + def test_commit_message(self): + self._commit_versions(versions.VersionInfo('1.0')) + commands.set_properties('1.0', [('foo.bar', True)], + message='commit message') + + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_properties([ + versions.VersionInfo('1.0', properties={'foo': {'bar': True}}), + ], expected_message='commit message') + + def test_deploy_prefix(self): + self._commit_versions(versions.VersionInfo('1.0'), + deploy_prefix='prefix') + commands.set_properties('1.0', [('foo.bar', True)], + deploy_prefix='prefix') + + check_call_silent(['git', 'checkout', 'gh-pages']) + self._test_set_properties([ + versions.VersionInfo('1.0', properties={'foo': {'bar': True}}), + ], directory='prefix') + + def test_invalid_version(self): + self._commit_versions(versions.VersionInfo('1.0')) + with self.assertRaises(ValueError): + commands.set_properties('2.0', [('foo.bar', True)]) + + class TestRetitle(unittest.TestCase): def setUp(self): self.stage = stage_dir('retitle')