Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable-2.15] Remove hacking dir dependency from build backend #81025

Merged
merged 2 commits into from
Jun 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ include examples/ansible.cfg
include examples/scripts/ConfigureRemotingForAnsible.ps1
include examples/scripts/upgrade_to_ps3.ps1
recursive-include licenses *.txt
recursive-include packaging *.py
recursive-include packaging *.py *.j2
recursive-include test/integration *
recursive-include test/sanity *.in *.json *.py *.txt
recursive-include test/support *.py *.ps1 *.psm1 *.cs *.md
Expand Down
5 changes: 2 additions & 3 deletions packaging/pep517_backend/_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ def _generate_rst_in_templates() -> t.Iterable[Path]:
"""Create ``*.1.rst.in`` files out of CLI Python modules."""
generate_man_cmd = (
sys.executable,
'hacking/build-ansible.py',
'generate-man',
Path(__file__).parent / '_generate_man.py',
'--output-dir=docs/man/man1/',
'--output-format=man',
*Path('lib/ansible/cli/').glob('*.py'),
Expand Down Expand Up @@ -162,7 +161,7 @@ def get_requires_for_build_sdist(

manpage_build_deps = [
'docutils', # provides `rst2man`
'jinja2', # used in `hacking/build-ansible.py generate-man`
'jinja2', # used to generate man pages
'pyyaml', # needed for importing in-tree `ansible-core` from `lib/`
] if build_manpages_requested else []

Expand Down
310 changes: 310 additions & 0 deletions packaging/pep517_backend/_generate_man.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
# coding: utf-8
# Copyright: (c) 2019, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Generate cli documentation from cli docstrings."""

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type


import argparse
import os.path
import pathlib
import sys

from jinja2 import Environment, FileSystemLoader

DEFAULT_TEMPLATE_FILE = pathlib.Path(__file__).parent / '_templates/man.j2'


# from https://www.python.org/dev/peps/pep-0257/
def trim_docstring(docstring):
if not docstring:
return ''
# Convert tabs to spaces (following the normal Python rules)
# and split into a list of lines:
lines = docstring.expandtabs().splitlines()
# Determine minimum indentation (first line doesn't count):
indent = sys.maxsize
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < sys.maxsize:
for line in lines[1:]:
trimmed.append(line[indent:].rstrip())
# Strip off trailing and leading blank lines:
while trimmed and not trimmed[-1]:
trimmed.pop()
while trimmed and not trimmed[0]:
trimmed.pop(0)
# Return a single string:
return '\n'.join(trimmed)


def get_options(optlist):
''' get actual options '''

opts = []
for opt in optlist:
res = {
'desc': opt.help,
'options': opt.option_strings
}
if isinstance(opt, argparse._StoreAction):
res['arg'] = opt.dest.upper()
elif not res['options']:
continue
opts.append(res)

return opts


def dedupe_groups(parser):
action_groups = []
for action_group in parser._action_groups:
found = False
for a in action_groups:
if a._actions == action_group._actions:
found = True
break
if not found:
action_groups.append(action_group)
return action_groups


def get_option_groups(option_parser):
groups = []
for action_group in dedupe_groups(option_parser)[1:]:
group_info = {}
group_info['desc'] = action_group.description
group_info['options'] = action_group._actions
group_info['group_obj'] = action_group
groups.append(group_info)
return groups


def opt_doc_list(parser):
''' iterate over options lists '''

results = []
for option_group in dedupe_groups(parser)[1:]:
results.extend(get_options(option_group._actions))

results.extend(get_options(parser._actions))

return results


# def opts_docs(cli, name):
def opts_docs(cli_class_name, cli_module_name):
''' generate doc structure from options '''

cli_name = 'ansible-%s' % cli_module_name
if cli_module_name == 'adhoc':
cli_name = 'ansible'

# WIth no action/subcommand
# shared opts set
# instantiate each cli and ask its options
cli_klass = getattr(__import__("ansible.cli.%s" % cli_module_name,
fromlist=[cli_class_name]), cli_class_name)
cli = cli_klass([cli_name])

# parse the common options
try:
cli.init_parser()
except Exception:
pass

# base/common cli info
docs = {
'cli': cli_module_name,
'cli_name': cli_name,
'usage': cli.parser.format_usage(),
'short_desc': cli.parser.description,
'long_desc': trim_docstring(cli.__doc__),
'actions': {},
'content_depth': 2,
}
option_info = {'option_names': [],
'options': [],
'groups': []}

for extras in ('ARGUMENTS'):
if hasattr(cli, extras):
docs[extras.lower()] = getattr(cli, extras)

common_opts = opt_doc_list(cli.parser)
groups_info = get_option_groups(cli.parser)
shared_opt_names = []
for opt in common_opts:
shared_opt_names.extend(opt.get('options', []))

option_info['options'] = common_opts
option_info['option_names'] = shared_opt_names

option_info['groups'].extend(groups_info)

docs.update(option_info)

# now for each action/subcommand
# force populate parser with per action options

def get_actions(parser, docs):
# use class attrs not the attrs on a instance (not that it matters here...)
try:
subparser = parser._subparsers._group_actions[0].choices
except AttributeError:
subparser = {}

depth = 0

for action, parser in subparser.items():
action_info = {'option_names': [],
'options': [],
'actions': {}}
# docs['actions'][action] = {}
# docs['actions'][action]['name'] = action
action_info['name'] = action
action_info['desc'] = trim_docstring(getattr(cli, 'execute_%s' % action).__doc__)

# docs['actions'][action]['desc'] = getattr(cli, 'execute_%s' % action).__doc__.strip()
action_doc_list = opt_doc_list(parser)

uncommon_options = []
for action_doc in action_doc_list:
# uncommon_options = []

option_aliases = action_doc.get('options', [])
for option_alias in option_aliases:

if option_alias in shared_opt_names:
continue

# TODO: use set
if option_alias not in action_info['option_names']:
action_info['option_names'].append(option_alias)

if action_doc in action_info['options']:
continue

uncommon_options.append(action_doc)

action_info['options'] = uncommon_options

depth = 1 + get_actions(parser, action_info)

docs['actions'][action] = action_info

return depth

action_depth = get_actions(cli.parser, docs)
docs['content_depth'] = action_depth + 1

docs['options'] = opt_doc_list(cli.parser)
return docs


class GenerateMan:
name = 'generate-man'

@classmethod
def init_parser(cls, parser: argparse.ArgumentParser):
parser.add_argument("-t", "--template-file", action="store", dest="template_file",
default=DEFAULT_TEMPLATE_FILE, help="path to jinja2 template")
parser.add_argument("-o", "--output-dir", action="store", dest="output_dir",
default='/tmp/', help="Output directory for rst files")
parser.add_argument("-f", "--output-format", action="store", dest="output_format",
default='man',
help="Output format for docs (the default 'man' or 'rst')")
parser.add_argument('cli_modules', help='CLI module name(s)', metavar='MODULE_NAME', nargs='*')

@staticmethod
def main(args):
template_file = args.template_file
template_path = os.path.expanduser(template_file)
template_dir = os.path.abspath(os.path.dirname(template_path))
template_basename = os.path.basename(template_file)

output_dir = os.path.abspath(args.output_dir)
output_format = args.output_format

cli_modules = args.cli_modules

# various cli parsing things checks sys.argv if the 'args' that are passed in are []
# so just remove any args so the cli modules dont try to parse them resulting in warnings
sys.argv = [sys.argv[0]]

allvars = {}
output = {}
cli_list = []
cli_bin_name_list = []

# for binary in os.listdir('../../lib/ansible/cli'):
for cli_module_name in cli_modules:
binary = os.path.basename(os.path.expanduser(cli_module_name))

if not binary.endswith('.py'):
continue
elif binary == '__init__.py':
continue

cli_name = os.path.splitext(binary)[0]

if cli_name == 'adhoc':
cli_class_name = 'AdHocCLI'
# myclass = 'AdHocCLI'
output[cli_name] = 'ansible.1.rst.in'
cli_bin_name = 'ansible'
else:
# myclass = "%sCLI" % libname.capitalize()
cli_class_name = "%sCLI" % cli_name.capitalize()
output[cli_name] = 'ansible-%s.1.rst.in' % cli_name
cli_bin_name = 'ansible-%s' % cli_name

# FIXME:
allvars[cli_name] = opts_docs(cli_class_name, cli_name)
cli_bin_name_list.append(cli_bin_name)

cli_list = allvars.keys()

doc_name_formats = {'man': '%s.1.rst.in',
'rst': '%s.rst'}

for cli_name in cli_list:

# template it!
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template(template_basename)

# add rest to vars
tvars = allvars[cli_name]
tvars['cli_list'] = cli_list
tvars['cli_bin_name_list'] = cli_bin_name_list
tvars['cli'] = cli_name
if '-i' in tvars['options']:
print('uses inventory')

manpage = template.render(tvars)
filename = os.path.join(output_dir, doc_name_formats[output_format] % tvars['cli_name'])
pathlib.Path(filename).write_text(manpage)


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)

GenerateMan.init_parser(parser)

args = parser.parse_args()

sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent / 'lib'))

GenerateMan.main(args)


if __name__ == '__main__':
main()
Loading