Skip to content

Commit

Permalink
Allow setting arbitrary properties for versions; resolves #138
Browse files Browse the repository at this point in the history
  • Loading branch information
jimporter committed Nov 1, 2023
1 parent 27e7cba commit 435d88e
Show file tree
Hide file tree
Showing 9 changed files with 731 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions mike/arguments.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from argparse import *
import re as _re
import textwrap as _textwrap

_ArgumentParser = ArgumentParser
_Action = Action
Expand All @@ -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)
Expand Down
42 changes: 41 additions & 1 deletion mike/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' +
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down
90 changes: 86 additions & 4 deletions mike/driver.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions test/integration/test_command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 10 additions & 0 deletions test/integration/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down
Loading

0 comments on commit 435d88e

Please sign in to comment.