Skip to content

Commit c69d11f

Browse files
committed
Allow setting arbitrary properties for versions; resolves #138
1 parent 27e7cba commit c69d11f

File tree

9 files changed

+734
-7
lines changed

9 files changed

+734
-7
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### New features
66

7+
- Add support for applying arbitrary properties to documentation versions
78
- Deploy aliases using symbolic links by default; this can be configured via
89
`--alias-type` on the command line or `alias_type` in the `mike` MkDocs plugin
910
- Avoid creating empty commits by default; if you want empty commits, pass

README.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ redirect template with `-T`/`--template`; this takes a path to a [Jinja][jinja]
132132
template that accepts an `{{href}}` variable.
133133

134134
If you'd like to specify a title for this version that doesn't match the version
135-
string, you can pass `-t TITLE`/`--title=TITLE` as well.
135+
string, you can pass `-t TITLE`/`--title=TITLE` as well. You can set custom
136+
properties for this version as well, using `--prop-set`, `--prop-set-string`,
137+
`--prop-set-all`, `--prop-delete`, and `--prop-delete-all` (see the [Managing
138+
Properties](#managing-properties) section for more details).
136139

137140
In addition, you can specify where to deploy your docs via `-b`/`--branch`,
138141
`-r`/`--remote`, and `--deploy-prefix`, specifying the branch, remote, and
@@ -240,6 +243,44 @@ existing alias points to.
240243
Once again, you can specify `--branch`, `--push`, etc to control how the commit
241244
is handled.
242245

246+
### Managing Properties
247+
248+
Each version of your documentation can have any arbitrary properties assigned to
249+
it that you like. You can use these properties to hold extra metadata, and then
250+
your documentation theme can consult those properties to do whatever you like.
251+
You can get properties via `props` command:
252+
253+
```sh
254+
mike props [identifier] [prop]
255+
```
256+
257+
If `prop` is specified, this will return the value of that property; otherwise,
258+
it will return all of that version's properties as a JSON object.
259+
260+
You can also set properties by specifying one or more of `--set prop=json`,
261+
`--set-string prop=str`, `--set-all json`, `--delete prop`, and `--delete-all`.
262+
(If you prefer, you can also set properties at the same time as deploying via
263+
the `--prop-*` options.)
264+
265+
When getting or setting a particular property, you can specify it with a
266+
limited JSONPath-like syntax. You can use bare field names, quoted field
267+
names, and indices/field names inside square brackets. The only operator
268+
supported is `.`. For example, this is a valid expression:
269+
270+
```javascript
271+
foo."bar"[0].["baz"]
272+
```
273+
274+
When setting values, you can add to the head or tail of a list via the `head`
275+
or `tail` keywords, e.g.:
276+
277+
```javascript
278+
foo[head]
279+
```
280+
281+
As usual, you can specify `--branch`, `--push`, etc to control how the commit is
282+
handled.
283+
243284
### More Details
244285

245286
For more details on the available options, consult the `--help` command for
@@ -314,10 +355,15 @@ this:
314355
```js
315356
[
316357
{"version": "1.0", "title": "1.0.1", "aliases": ["latest"]},
317-
{"version": "0.9", "title": "0.9", "aliases": []}
358+
{"version": "0.9", "title": "0.9", "aliases": [], properties: "anything"}
318359
]
319360
```
320361

362+
Every version has a `version` string, a `title` (which may be the same as
363+
`version`), a list of `aliases`, and optionally, a `properties` attribute that
364+
can hold anything at all. These properties can be used by other packages,
365+
themes, etc in order to add their own custom metadata to each version.
366+
321367
If you're creating a third-party extension to an existing theme, you add a
322368
setuptools entry point for `mike.themes` pointing to a Python submodule that
323369
contains `css/` and `js/` subdirectories containing the extra code to be

mike/arguments.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from argparse import *
2+
import re as _re
3+
import textwrap as _textwrap
24

35
_ArgumentParser = ArgumentParser
46
_Action = Action
@@ -13,6 +15,13 @@ def _add_complete(argument, complete):
1315
return argument
1416

1517

18+
class ParagraphDescriptionHelpFormatter(HelpFormatter):
19+
def _fill_text(self, text, width, indent):
20+
# Re-fill text, but keep paragraphs. Why isn't this the default???
21+
return '\n\n'.join(_textwrap.fill(i, width) for i in
22+
_re.split('\n\n', text.strip()))
23+
24+
1625
class Action(_Action):
1726
def __init__(self, *args, complete=None, **kwargs):
1827
super().__init__(*args, **kwargs)

mike/commands.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def make_nojekyll():
6262
@contextmanager
6363
def deploy(cfg, version, title=None, aliases=[], update_aliases=False,
6464
alias_type=AliasType.symlink, template=None, *, branch='gh-pages',
65-
message=None, allow_empty=False, deploy_prefix=''):
65+
message=None, allow_empty=False, deploy_prefix='', set_props=[]):
6666
if message is None:
6767
message = (
6868
'Deployed {rev} to {doc_version}{deploy_prefix} with MkDocs ' +
@@ -81,6 +81,9 @@ def deploy(cfg, version, title=None, aliases=[], update_aliases=False,
8181
destdir = os.path.join(deploy_prefix, version_str)
8282
alias_destdirs = [os.path.join(deploy_prefix, i) for i in info.aliases]
8383

84+
for path, value in set_props:
85+
info.set_property(path, value)
86+
8487
# Let the caller perform the build.
8588
yield
8689

@@ -209,6 +212,43 @@ def alias(cfg, identifier, aliases, update_aliases=False,
209212
commit.add_file(versions_to_file_info(all_versions, deploy_prefix))
210213

211214

215+
def get_property(identifier, prop, *, branch='gh-pages', deploy_prefix=''):
216+
all_versions = list_versions(branch, deploy_prefix)
217+
try:
218+
real_version = all_versions.find(identifier, strict=True)[0]
219+
info = all_versions[real_version]
220+
except KeyError as e:
221+
raise ValueError('identifier {} does not exist'.format(e))
222+
223+
return info.get_property(prop)
224+
225+
226+
def set_properties(identifier, set_props, *, branch='gh-pages', message=None,
227+
allow_empty=False, deploy_prefix=''):
228+
all_versions = list_versions(branch, deploy_prefix)
229+
try:
230+
real_version = all_versions.find(identifier, strict=True)[0]
231+
info = all_versions[real_version]
232+
except KeyError as e:
233+
raise ValueError('identifier {} does not exist'.format(e))
234+
235+
if message is None:
236+
message = (
237+
'Set properties for {doc_version}{deploy_prefix} with mike ' +
238+
'{mike_version}'
239+
).format(
240+
doc_version=real_version,
241+
deploy_prefix=_format_deploy_prefix(deploy_prefix),
242+
mike_version=app_version
243+
)
244+
245+
for path, value in set_props:
246+
info.set_property(path, value)
247+
248+
with git_utils.Commit(branch, message, allow_empty=allow_empty) as commit:
249+
commit.add_file(versions_to_file_info(all_versions, deploy_prefix))
250+
251+
212252
def retitle(identifier, title, *, branch='gh-pages', message=None,
213253
allow_empty=False, deploy_prefix=''):
214254
if message is None:

mike/driver.py

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import json
12
import os
23
import sys
34
from contextlib import contextmanager
45

5-
from . import arguments, commands
6+
from . import arguments
7+
from . import commands
68
from . import git_utils
9+
from . import jsonpath
710
from . import mkdocs_utils
811
from .app_version import version as app_version
912
from .mkdocs_plugin import MikePlugin
@@ -32,6 +35,22 @@
3235
the target branch.
3336
"""
3437

38+
props_desc = """
39+
Get or set properties for the specified version.
40+
41+
When getting or setting a particular property, you can specify it with a
42+
limited JSONPath-like syntax. You can use bare field names, quoted field names,
43+
and indices/field names inside square brackets. The only operator supported is
44+
`.`. For example, this is a valid expression:
45+
46+
foo."bar"[0].["baz"]
47+
48+
When setting values, you can add to the head or tail of a list via the `head`
49+
or `tail` keywords, e.g.:
50+
51+
foo[head]
52+
"""
53+
3554
retitle_desc = """
3655
Change the descriptive title of the specified version of the documentation on
3756
the target branch.
@@ -80,13 +99,40 @@ def add_git_arguments(parser, *, commit=True, deploy_prefix=True):
8099
if deploy_prefix:
81100
git.add_argument('--deploy-prefix', metavar='PATH',
82101
complete='directory',
83-
help=('subdirectory within {branch} where generated '
84-
'docs should be deployed to'))
102+
help=('subdirectory within {branch} where ' +
103+
'generated docs should be deployed to'))
85104

86105
git.add_argument('--ignore-remote-status', action='store_true',
87106
help="don't check status of remote branch")
88107

89108

109+
def add_set_prop_arguments(parser, *, prefix=''):
110+
def parse_set_json(expression):
111+
result = jsonpath.parse_set(expression)
112+
return result[0], json.loads(result[1])
113+
114+
prop_p = parser.add_argument_group('property manipulation arguments')
115+
prop_p.add_argument('--{}set'.format(prefix), metavar='PROP=JSON',
116+
action='append', type=parse_set_json, dest='set_props',
117+
help='set the property at PROP to a JSON value')
118+
prop_p.add_argument('--{}set-string'.format(prefix), metavar='PROP=STRING',
119+
action='append', type=jsonpath.parse_set,
120+
dest='set_props',
121+
help='set the property at PROP to a STRING value')
122+
prop_p.add_argument('--{}set-all'.format(prefix), metavar='JSON',
123+
action='append', type=lambda x: ('', json.loads(x)),
124+
dest='set_props',
125+
help='set all properties to a JSON value')
126+
prop_p.add_argument('--{}delete'.format(prefix), metavar='PROP',
127+
action='append',
128+
type=lambda x: (jsonpath.parse(x), jsonpath.Deleted),
129+
dest='set_props', help='delete the property at PROP')
130+
prop_p.add_argument('--{}delete-all'.format(prefix),
131+
action='append_const', const=('', jsonpath.Deleted),
132+
dest='set_props', help='delete all properties')
133+
return prop_p
134+
135+
90136
def load_mkdocs_config(args, strict=False):
91137
def maybe_set(args, cfg, field, cfg_field=None):
92138
if getattr(args, field, object()) is None:
@@ -149,7 +195,8 @@ def deploy(parser, args):
149195
args.update_aliases, alias_type, args.template,
150196
branch=args.branch, message=args.message,
151197
allow_empty=args.allow_empty,
152-
deploy_prefix=args.deploy_prefix), \
198+
deploy_prefix=args.deploy_prefix,
199+
set_props=args.set_props or []), \
153200
mkdocs_utils.inject_plugin(args.config_file) as config_file:
154201
mkdocs_utils.build(config_file, args.version)
155202
if args.push:
@@ -179,6 +226,28 @@ def alias(parser, args):
179226
git_utils.push_branch(args.remote, args.branch)
180227

181228

229+
def props(parser, args):
230+
load_mkdocs_config(args)
231+
check_remote_status(args, strict=args.set_props)
232+
233+
if args.get_prop and args.set_props:
234+
raise ValueError('cannot get and set properties at the same time')
235+
elif args.set_props:
236+
with handle_empty_commit():
237+
commands.set_properties(args.identifier, args.set_props,
238+
branch=args.branch, message=args.message,
239+
allow_empty=args.allow_empty,
240+
deploy_prefix=args.deploy_prefix)
241+
if args.push:
242+
git_utils.push_branch(args.remote, args.branch)
243+
else:
244+
print(json.dumps(
245+
commands.get_property(args.identifier, args.get_prop,
246+
branch=args.branch,
247+
deploy_prefix=args.deploy_prefix)
248+
))
249+
250+
182251
def retitle(parser, args):
183252
load_mkdocs_config(args)
184253
check_remote_status(args, strict=True)
@@ -282,6 +351,7 @@ def main():
282351
deploy_p.add_argument('-T', '--template', complete='file',
283352
help='template file to use for redirects')
284353
add_git_arguments(deploy_p)
354+
add_set_prop_arguments(deploy_p, prefix='prop-')
285355
deploy_p.add_argument('version', metavar='VERSION',
286356
help='version to deploy this build to')
287357
deploy_p.add_argument('aliases', nargs='*', metavar='ALIAS',
@@ -315,6 +385,18 @@ def main():
315385
alias_p.add_argument('aliases', nargs='*', metavar='ALIAS',
316386
help='new alias to add')
317387

388+
props_p = subparsers.add_parser(
389+
'props', description=props_desc, help='get/set version properties',
390+
formatter_class=arguments.ParagraphDescriptionHelpFormatter
391+
)
392+
props_p.set_defaults(func=props)
393+
add_git_arguments(props_p)
394+
add_set_prop_arguments(props_p)
395+
props_p.add_argument('identifier', metavar='IDENTIFIER',
396+
help='existing version or alias')
397+
props_p.add_argument('get_prop', nargs='?', metavar='PROP', default='',
398+
help='property to get')
399+
318400
retitle_p = subparsers.add_parser(
319401
'retitle', description=retitle_desc,
320402
help='change the title of a version'

test/integration/test_command_line.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ def test_help_subcommand_extra(self):
2020
'--ignore-remote-status'])
2121
self.assertRegex(output, r'^usage: mike deploy')
2222

23+
def test_help_paragraph_formatter(self):
24+
output = assertPopen(['mike', 'help', 'props'])
25+
self.assertRegex(output, r'^usage: mike props')
26+
self.assertRegex(output, ('(?m)^Get or set properties for the ' +
27+
'specified version\\.\n\nWhen getting'))
28+
2329

2430
class GenerateCompletionTest(unittest.TestCase):
2531
def test_completion(self):

test/integration/test_deploy.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ def test_aliases_copy(self):
105105
versions.VersionInfo('1.0', aliases=['latest'])
106106
], alias_type=AliasType.copy)
107107

108+
def test_props(self):
109+
assertPopen(['mike', 'deploy', '1.0',
110+
'--prop-set', 'foo.bar=[1,2,3]',
111+
'--prop-set', 'foo.bar[1]=true',
112+
'--prop-delete', 'foo.bar[0]'])
113+
check_call_silent(['git', 'checkout', 'gh-pages'])
114+
self._test_deploy(expected_versions=[
115+
versions.VersionInfo('1.0', properties={'foo': {'bar': [True, 3]}})
116+
])
117+
108118
def test_update(self):
109119
assertPopen(['mike', 'deploy', '1.0', 'latest'])
110120
assertPopen(['mike', 'deploy', '1.0', 'greatest', '-t', '1.0.1'])

0 commit comments

Comments
 (0)