diff --git a/.travis.yml b/.travis.yml index 64c1aaa..e4fe1a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,15 @@ language: python python: - - '2.7' - - pypy - - '3.5' - - '3.6' - - '3.7' - - '3.8' - + - '2.7' + - '3.5' + - '3.6' + - '3.7' + - '3.8' + - pypy install: - - python setup.py build sdist - - pip install -r requirements-dev.txt - -script: nosetests --verbosity 2 - + - pip install tox-travis +script: + - tox deploy: provider: pypi user: tuxtimo diff --git a/click_man/__main__.py b/click_man/__main__.py index 4424605..1036141 100644 --- a/click_man/__main__.py +++ b/click_man/__main__.py @@ -2,68 +2,13 @@ click-man - Generate man pages for click application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This module provides a click CLI command to -generate man pages from a click application. +This module provides the ability to run the click man command via +``python -m click_main``. :copyright: (c) 2016 by Timo Furrer. :license: MIT, see LICENSE for more details. """ -import os -import click -from pkg_resources import iter_entry_points, get_distribution +from click_man.shell import cli -from click_man.core import write_man_pages - - -@click.command(context_settings={'help_option_names': ['-h', '--help']}) -@click.option('--target', '-t', default=os.path.join(os.getcwd(), 'man'), - type=click.Path(file_okay=False, dir_okay=True, resolve_path=True), - help='Target location for the man pages') -@click.version_option(get_distribution('click-man').version, '-V', '--version') -@click.argument('name') -def cli(target, name): - """ - Generate man pages for the scripts defined in the ``console_scripts`` entry point. - - The cli application is gathered from entry points of installed packages. - - The generated man pages are written to files in the directory given - by ``--target``. - """ - console_scripts = [ep for ep in iter_entry_points('console_scripts', name=name)] - if len(console_scripts) < 1: - raise click.ClickException('"{0}" is not an installed console script.'.format(name)) - # Only generate man pages for first console script - entry_point = console_scripts[0] - - # create target directory if it does not exist yet - try: - os.makedirs(target) - except OSError: - pass - - click.echo('Load entry point {0}'.format(name)) - cli = entry_point.resolve() - - # If the entry point isn't a click.Command object, try to find it in the module - if not isinstance(cli, click.Command): - from importlib import import_module - from inspect import getmembers - - if not entry_point.module_name: - raise click.ClickException('Could not find module name for "{0}".'.format(name)) - ep_module = import_module(entry_point.module_name) - ep_members = getmembers(ep_module, lambda x: isinstance(x, click.Command)) - - if len(ep_members) < 1: - raise click.ClickException('Could not find click.Command object for "{0}".'.format(name)) - (ep_name, cli) = ep_members[0] - click.echo('Found alternate entry point {0} in {1}'.format(ep_name, name)) - - click.echo('Generate man pages for {0} in {1}'.format(name, target)) - write_man_pages(name, cli, version=entry_point.dist.version, target_dir=target) - - -if __name__ == '__main__': - cli() +cli() diff --git a/click_man/core.py b/click_man/core.py index 73b431d..aa40b37 100644 --- a/click_man/core.py +++ b/click_man/core.py @@ -26,15 +26,17 @@ def get_short_help_str(command, limit=45): return command.short_help or command.help and click.utils.make_default_short_help(command.help, limit) or '' -def generate_man_page(ctx, version=None): +def generate_man_page(ctx, version=None, date=None): """ Generate documentation for the given command. :param click.Context ctx: the click context for the - cli application. + cli application. + :param str version: The version information to include in the man page. + :param str date: The date information to include in the man page. - :rtype: str :returns: the generate man page from the given click Context. + :rtype: str """ # Create man page with the details from the given context man_page = ManPage(ctx.command_path) @@ -42,7 +44,14 @@ def generate_man_page(ctx, version=None): man_page.short_help = get_short_help_str(ctx.command) man_page.description = ctx.command.help man_page.synopsis = ' '.join(ctx.command.collect_usage_pieces(ctx)) - man_page.options = [x.get_help_record(ctx) for x in ctx.command.params if isinstance(x, click.Option)] + man_page.options = [ + x.get_help_record(ctx) for x in ctx.command.params + if isinstance(x, click.Option) + ] + + if date: + man_page.date = date + commands = getattr(ctx.command, 'commands', None) if commands: man_page.commands = [ @@ -52,7 +61,9 @@ def generate_man_page(ctx, version=None): return str(man_page) -def write_man_pages(name, cli, parent_ctx=None, version=None, target_dir=None): +def write_man_pages( + name, cli, parent_ctx=None, version=None, target_dir=None, date=None, +): """ Generate man page files recursively for the given click cli function. @@ -62,6 +73,7 @@ def write_man_pages(name, cli, parent_ctx=None, version=None, target_dir=None): :param click.Context parent_ctx: the parent click context :param str target_dir: the directory where the generated man pages are stored. + :param date: the date to include in the header """ ctx = click.Context(cli, info_name=name, parent=parent_ctx) @@ -80,4 +92,8 @@ def write_man_pages(name, cli, parent_ctx=None, version=None, target_dir=None): if command.hidden: # Do not write a man page for a hidden command continue - write_man_pages(name, command, parent_ctx=ctx, version=version, target_dir=target_dir) + + write_man_pages( + name, command, parent_ctx=ctx, version=version, + target_dir=target_dir, date=date, + ) diff --git a/click_man/shell.py b/click_man/shell.py new file mode 100644 index 0000000..06587b6 --- /dev/null +++ b/click_man/shell.py @@ -0,0 +1,100 @@ +""" +click-man - Generate man pages for click application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module provides a click CLI command to +generate man pages from a click application. + +:copyright: (c) 2016 by Timo Furrer. +:license: MIT, see LICENSE for more details. +""" + +from datetime import datetime +import os +from pkg_resources import iter_entry_points, get_distribution + +import click + +from click_man.core import write_man_pages + + +@click.command(context_settings={'help_option_names': ['-h', '--help']}) +@click.option( + '--target', '-t', default=os.path.join(os.getcwd(), 'man'), + type=click.Path(file_okay=False, dir_okay=True, resolve_path=True), + help='Target location for the man pages' +) +@click.option('--man-version', help='Version to use in generated man page(s)') +@click.option('--man-date', help='Date to use in generated man page(s)') +@click.version_option(get_distribution('click-man').version, '-V', '--version') +@click.argument('name') +def cli(target, name, man_version, man_date): + """ + Generate man pages for the scripts defined in the ``console_scripts`` entry + point. + + The cli application is gathered from entry points of installed packages. + + The generated man pages are written to files in the directory given + by ``--target``. + """ + console_scripts = [ + ep for ep in iter_entry_points('console_scripts', name=name) + ] + if len(console_scripts) < 1: + raise click.ClickException( + '"{0}" is not an installed console script.'.format(name) + ) + # Only generate man pages for first console script + entry_point = console_scripts[0] + + # create target directory if it does not exist yet + try: + os.makedirs(target) + except OSError: + pass + + if not man_version: + man_version = entry_point.dist.version + + if man_date: + try: + datetime.strptime(man_date, '%Y-%m-%d') + except ValueError: + raise click.ClickException( + '"{0}" is not a valid date.'.format(man_date) + ) + + click.echo('Load entry point {0}'.format(name)) + cli = entry_point.resolve() + + # If the entry point isn't a click.Command object, try to find it in the + # module + if not isinstance(cli, click.Command): + from importlib import import_module + from inspect import getmembers + + if not entry_point.module_name: + raise click.ClickException( + 'Could not find module name for "{0}".'.format(name) + ) + + ep_module = import_module(entry_point.module_name) + ep_members = getmembers( + ep_module, lambda x: isinstance(x, click.Command), + ) + + if len(ep_members) < 1: + raise click.ClickException( + 'Could not find click.Command object for "{0}".'.format(name) + ) + + ep_name, cli = ep_members[0] + click.echo( + 'Found alternate entry point {0} in {1}'.format(ep_name, name) + ) + + click.echo('Generate man pages for {0} in {1}'.format(name, target)) + write_man_pages( + name, cli, version=man_version, target_dir=target, date=man_date, + ) diff --git a/tests/test_shell.py b/tests/test_shell.py new file mode 100644 index 0000000..b61df2a --- /dev/null +++ b/tests/test_shell.py @@ -0,0 +1,86 @@ +""" +Module to test CLI functionality of click-man package. +""" + +import os + +import click +from click.testing import CliRunner as CLIRunner +import mock + +from click_man import shell + + +@mock.patch.object(shell, 'iter_entry_points') +def test_missing_entry_point(mock_entry_points): + mock_entry_points.return_value = iter([]) + + runner = CLIRunner() + result = runner.invoke(shell.cli, 'foo') + + assert result.exit_code == 1, result + assert 'not an installed console script' in result.output.strip() + + mock_entry_points.assert_called_once_with('console_scripts', name='foo') + + +@mock.patch('os.makedirs', new=mock.Mock()) +@mock.patch.object(shell, 'write_man_pages') +@mock.patch.object(click, 'echo') +@mock.patch.object(shell, 'iter_entry_points') +def test_is_click_command(mock_entry_points, mock_echo, mock_write): + fake_target = os.path.join(os.getcwd(), 'man') + fake_command = click.Command(name='foo') + fake_version = '1.2.3' + entry_point = mock.Mock() + entry_point.resolve.return_value = fake_command + entry_point.dist.version = fake_version + + mock_entry_points.return_value = iter([entry_point]) + + runner = CLIRunner() + result = runner.invoke(shell.cli, ['foo']) + + assert result.exit_code == 0, result.output + + mock_entry_points.assert_called_once_with('console_scripts', name='foo') + mock_echo.assert_has_calls([ + mock.call('Load entry point foo'), + mock.call('Generate man pages for foo in %s' % fake_target), + ]) + mock_write.assert_called_once_with( + 'foo', fake_command, version=fake_version, target_dir=fake_target, + date=None, + ) + + +@mock.patch('os.makedirs', new=mock.Mock()) +@mock.patch.object(shell, 'write_man_pages') +@mock.patch.object(click, 'echo') +@mock.patch.object(shell, 'iter_entry_points') +def test_man_date_version(mock_entry_points, mock_echo, mock_write): + fake_target = os.path.join(os.getcwd(), 'man') + fake_command = click.Command(name='foo') + entry_point = mock.Mock() + entry_point.resolve.return_value = fake_command + + mock_entry_points.return_value = iter([entry_point]) + + runner = CLIRunner() + result = runner.invoke( + shell.cli, + ['foo', '--man-version', '3.2.1', '--man-date', '2020-01-01'], + ) + + assert result.exit_code == 0, result.output + + mock_entry_points.assert_called_once_with('console_scripts', name='foo') + entry_point.dist.version.assert_not_called() + mock_echo.assert_has_calls([ + mock.call('Load entry point foo'), + mock.call('Generate man pages for foo in %s' % fake_target), + ]) + mock_write.assert_called_once_with( + 'foo', fake_command, version='3.2.1', target_dir=fake_target, + date='2020-01-01', + )