Add Sphinx extension to pretty-print modules

This will provide a more parsable sample policy file for us in docs.
This compliments the 'sphinxpolicygen' module.

Change-Id: Id99b21e7c0a66aaf4223e60074626914235e0ca5
This commit is contained in:
Stephen Finucane 2017-04-27 12:37:31 +01:00
parent a7ab49e42c
commit 2320ee61a3
3 changed files with 250 additions and 11 deletions

View File

@ -21,19 +21,19 @@ from oslo_policy import policy
LOG = logging.getLogger(__name__)
_generator_opts = [
GENERATOR_OPTS = [
cfg.StrOpt('output-file',
help='Path of the file to write to. Defaults to stdout.'),
]
_rule_opts = [
RULE_OPTS = [
cfg.MultiStrOpt('namespace',
required=True,
help='Option namespace(s) under "oslo.policy.policies" in '
'which to query for options.'),
]
_enforcer_opts = [
ENFORCER_OPTS = [
cfg.StrOpt('namespace',
required=True,
help='Option namespace under "oslo.policy.enforcer" in '
@ -41,7 +41,7 @@ _enforcer_opts = [
]
def _get_policies_dict(namespaces):
def get_policies_dict(namespaces):
"""Find the options available via the given namespaces.
:param namespaces: a list of namespaces registered under
@ -156,7 +156,7 @@ def _generate_sample(namespaces, output_file=None, include_help=True):
along with rules in which everything is commented out.
False, generates a sample-policy file with only rules.
"""
policies = _get_policies_dict(namespaces)
policies = get_policies_dict(namespaces)
output_file = (open(output_file, 'w') if output_file
else sys.stdout)
@ -218,8 +218,8 @@ def on_load_failure_callback(*args, **kwargs):
def generate_sample(args=None):
logging.basicConfig(level=logging.WARN)
conf = cfg.ConfigOpts()
conf.register_cli_opts(_generator_opts + _rule_opts)
conf.register_opts(_generator_opts + _rule_opts)
conf.register_cli_opts(GENERATOR_OPTS + RULE_OPTS)
conf.register_opts(GENERATOR_OPTS + RULE_OPTS)
conf(args)
_generate_sample(conf.namespace, conf.output_file)
@ -227,8 +227,8 @@ def generate_sample(args=None):
def generate_policy(args=None):
logging.basicConfig(level=logging.WARN)
conf = cfg.ConfigOpts()
conf.register_cli_opts(_generator_opts + _enforcer_opts)
conf.register_opts(_generator_opts + _enforcer_opts)
conf.register_cli_opts(GENERATOR_OPTS + ENFORCER_OPTS)
conf.register_opts(GENERATOR_OPTS + ENFORCER_OPTS)
conf(args)
_generate_policy(conf.namespace, conf.output_file)
@ -236,7 +236,7 @@ def generate_policy(args=None):
def list_redundant(args=None):
logging.basicConfig(level=logging.WARN)
conf = cfg.ConfigOpts()
conf.register_cli_opts(_enforcer_opts)
conf.register_opts(_enforcer_opts)
conf.register_cli_opts(ENFORCER_OPTS)
conf.register_opts(ENFORCER_OPTS)
conf(args)
_list_redundant(conf.namespace)

157
oslo_policy/sphinxext.py Normal file
View File

@ -0,0 +1,157 @@
# Copyright 2017 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Sphinx extension for pretty-formatting policy docs."""
import os
from docutils import nodes
from docutils.parsers import rst
from docutils.parsers.rst import directives
from docutils import statemachine
from oslo_config import cfg
from sphinx.util.nodes import nested_parse_with_titles
from oslo_policy import generator
def _indent(text):
"""Indent by four spaces."""
prefix = ' ' * 4
def prefixed_lines():
for line in text.splitlines(True):
yield (prefix + line if line.strip() else line)
return ''.join(prefixed_lines())
def _format_policy_rule(rule):
"""Output a definition list-style rule.
For example:
``os_compute_api:servers:create``
Create a server
Default::
rule:admin_or_owner
Operations:
- **POST** ``/servers``
"""
yield '``{}``'.format(rule.name)
yield ''
if rule.description:
for line in statemachine.string2lines(
rule.description, tab_width=4, convert_whitespace=True):
yield _indent(line)
yield ''
yield _indent('Default::')
yield ''
yield _indent(_indent(rule.check_str))
if hasattr(rule, 'operations'):
yield ''
yield _indent('Operations:')
yield ''
for operation in rule.operations:
yield _indent('- **{}** ``{}``'.format(operation['method'],
operation['path']))
yield ''
def _format_policy_section(section, rules):
# The nested_parse_with_titles will ensure the correct header leve is used.
yield section
yield '=' * len(section)
yield ''
for rule in rules:
for line in _format_policy_rule(rule):
yield line
def _format_policy(namespaces):
policies = generator.get_policies_dict(namespaces)
for section in sorted(policies.keys()):
for line in _format_policy_section(section, policies[section]):
yield line
class ShowPolicyDirective(rst.Directive):
has_content = False
option_spec = {
'config-file': directives.unchanged,
}
def run(self):
env = self.state.document.settings.env
app = env.app
config_file = self.options.get('config-file')
# if the config_file option was not defined, attempt to reuse the
# 'oslo_policy.sphinxpolicygen' extension's setting
if not config_file and hasattr(env.config,
'policy_generator_config_file'):
config_file = env.config.policy_generator_config_file
# If we are given a file that isn't an absolute path, look for it
# in the source directory if it doesn't exist.
candidates = [
config_file,
os.path.join(app.srcdir, config_file,),
]
for c in candidates:
if os.path.isfile(c):
config_path = c
break
else:
self.error('could not find config file in: %s' % str(candidates))
self.info('loading config file %s' % config_path)
conf = cfg.ConfigOpts()
opts = generator.GENERATOR_OPTS + generator.RULE_OPTS
conf.register_cli_opts(opts)
conf.register_opts(opts)
conf(
args=['--config-file', config_path],
)
namespaces = conf.namespace[:]
result = statemachine.ViewList()
source_name = '<' + __name__ + '>'
for line in _format_policy(namespaces):
result.append(line, source_name)
node = nodes.section()
node.document = self.state.document
nested_parse_with_titles(self.state, result, node)
return node.children
def setup(app):
app.add_directive('show-policy', ShowPolicyDirective)

View File

@ -0,0 +1,82 @@
# Copyright 2017 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import textwrap
from oslotest import base
from oslo_policy import policy
from oslo_policy import sphinxext
class FormatPolicyTest(base.BaseTestCase):
def test_minimal(self):
results = '\n'.join(list(sphinxext._format_policy_section(
'foo', [policy.RuleDefault('rule_a', '@')])))
self.assertEqual(textwrap.dedent("""
foo
===
``rule_a``
Default::
@
""").lstrip(), results)
def test_with_description(self):
results = '\n'.join(list(sphinxext._format_policy_section(
'foo', [policy.RuleDefault('rule_a', '@', 'My sample rule')]
)))
self.assertEqual(textwrap.dedent("""
foo
===
``rule_a``
My sample rule
Default::
@
""").lstrip(), results)
def test_with_operations(self):
results = '\n'.join(list(sphinxext._format_policy_section(
'foo', [policy.DocumentedRuleDefault(
'rule_a', '@', 'My sample rule', [
{'method': 'GET', 'path': '/foo'},
{'method': 'POST', 'path': '/some'}])]
)))
self.assertEqual(textwrap.dedent("""
foo
===
``rule_a``
My sample rule
Default::
@
Operations:
- **GET** ``/foo``
- **POST** ``/some``
""").lstrip(), results)