From 78dedc370c414bd3e86bec090de270c20b0f91a4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 17 Apr 2020 17:39:35 +0100 Subject: [PATCH 1/4] Move 'cli()' method to 'click_man.shell' It's easier to test from here and avoids us having loads of code in a dunder file. Some style fixups are included. Signed-off-by: Stephen Finucane --- click_man/__main__.py | 63 ++------------------------------ click_man/shell.py | 85 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 59 deletions(-) create mode 100644 click_man/shell.py 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/shell.py b/click_man/shell.py new file mode 100644 index 0000000..c8a3181 --- /dev/null +++ b/click_man/shell.py @@ -0,0 +1,85 @@ +""" +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. +""" + +import os +import click +from pkg_resources import iter_entry_points, get_distribution + +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, + ) From 5b643ac0898ed43404eef09cabebeeee323ea9bd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 17 Apr 2020 18:15:43 +0100 Subject: [PATCH 2/4] Add shell unit tests Signed-off-by: Stephen Finucane --- tests/test_shell.py | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_shell.py diff --git a/tests/test_shell.py b/tests/test_shell.py new file mode 100644 index 0000000..14d4bec --- /dev/null +++ b/tests/test_shell.py @@ -0,0 +1,53 @@ +""" +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, + ) From 7dcd7ba6d8a8ec4babc39b7e29c5511f47736fee Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 16 Sep 2019 11:45:26 +0100 Subject: [PATCH 3/4] cli: Expose knobs for date, version Provide a way to override the date and version used in generated man pages. This can be useful where you want to generate man pages before actually releasing something and don't want to manually edit them. Signed-off-by: Stephen Finucane --- click_man/core.py | 28 ++++++++++++++++++++++------ click_man/shell.py | 21 ++++++++++++++++++--- tests/test_shell.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) 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 index c8a3181..06587b6 100644 --- a/click_man/shell.py +++ b/click_man/shell.py @@ -9,10 +9,12 @@ :license: MIT, see LICENSE for more details. """ +from datetime import datetime import os -import click from pkg_resources import iter_entry_points, get_distribution +import click + from click_man.core import write_man_pages @@ -22,9 +24,11 @@ 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): +def cli(target, name, man_version, man_date): """ Generate man pages for the scripts defined in the ``console_scripts`` entry point. @@ -50,6 +54,17 @@ def cli(target, name): 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() @@ -81,5 +96,5 @@ def cli(target, 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, + name, cli, version=man_version, target_dir=target, date=man_date, ) diff --git a/tests/test_shell.py b/tests/test_shell.py index 14d4bec..b61df2a 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -50,4 +50,37 @@ def test_is_click_command(mock_entry_points, mock_echo, mock_write): ]) 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', ) From 2b2c1b1c50008e323898908170db4b4e8af51379 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 17 Apr 2020 19:43:59 +0100 Subject: [PATCH 4/4] travis: Resolve build issues The tests now rely on the package being installed. Do that by using tox to run our tests in Travis, like we do locally. Signed-off-by: Stephen Finucane --- .travis.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) 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