Utility functions
=================

.. :doctest:

We're testing ``utils.py`` here:

    >>> from zest.releaser import utils
    >>> from pprint import pprint


Log level
---------

A ``-v`` on the commandline turns on debug level logging:

    >>> import sys
    >>> import logging
    >>> sys.argv[1:] = []
    >>> utils.parse_options()
    >>> utils.VERBOSE
    False
    >>> utils.loglevel() == logging.INFO
    True
    >>> sys.argv[1:] = ['-v']
    >>> utils.parse_options()
    >>> utils.VERBOSE
    True
    >>> utils.loglevel() == logging.DEBUG
    True
    >>> sys.argv[1:] = []


Version numbers
---------------

Strip all whitespace in a version:

    >>> utils.strip_version('1.0')
    '1.0'
    >>> utils.strip_version(' 1.0   dev  ')
    '1.0dev'

Remove development markers in various common forms:

    >>> utils.cleanup_version('1.0')
    '1.0'
    >>> utils.cleanup_version('1.0 dev')
    '1.0'
    >>> utils.cleanup_version('1.0 (svn/devel)')
    '1.0'
    >>> utils.cleanup_version('1.0 svn')
    '1.0'
    >>> utils.cleanup_version('1.0 devel 13')
    '1.0'
    >>> utils.cleanup_version('1.0 beta devel 13')
    '1.0 beta'
    >>> utils.cleanup_version('1.0.dev0')
    '1.0'
    >>> utils.cleanup_version('1.0.dev42')
    '1.0'


Asking input
------------

Asking input on the prompt is not unittestable unless we use the prepared
testing hack in utils.py:

    >>> utils.TESTMODE = True
    >>> utils.answers_for_testing = []

The default is True, so hitting enter (which means no input) returns True

    >>> utils.answers_for_testing = ['']
    >>> utils.ask('Does mocking work?')
    Question: Does mocking work? (Y/n)?
    Our reply: <ENTER>
    True

A default of False also changes the Y/n to y/N:

    >>> utils.answers_for_testing = ['']
    >>> utils.ask('Does mocking work?', default=False)
    Question: Does mocking work? (y/N)?
    Our reply: <ENTER>
    False

A default of None requires an answer:

    >>> utils.answers_for_testing = ['', 'y']
    >>> utils.ask('Does mocking work?', default=None)
    Question: Does mocking work? (y/n)?
    Our reply: <ENTER>
    Please explicitly answer y/n
    Question: Does mocking work? (y/n)?
    Our reply: y
    True

Y and n can be upper or lower case:

    >>> utils.answers_for_testing = ['y']
    >>> utils.ask('Does mocking work?', default=None)
    Question: Does mocking work? (y/n)?
    Our reply: y
    True
    >>> utils.answers_for_testing = ['Y']
    >>> utils.ask('Does mocking work?', default=None)
    Question: Does mocking work? (y/n)?
    Our reply: Y
    True
    >>> utils.answers_for_testing = ['n']
    >>> utils.ask('Does mocking work?', default=None)
    Question: Does mocking work? (y/n)?
    Our reply: n
    False
    >>> utils.answers_for_testing = ['N']
    >>> utils.ask('Does mocking work?', default=None)
    Question: Does mocking work? (y/n)?
    Our reply: N
    False

Yes and no are fine:

    >>> utils.answers_for_testing = ['yes']
    >>> utils.ask('Does mocking work?', default=None)
    Question: Does mocking work? (y/n)?
    Our reply: yes
    True
    >>> utils.answers_for_testing = ['no']
    >>> utils.ask('Does mocking work?', default=None)
    Question: Does mocking work? (y/n)?
    Our reply: no
    False

The y or n must be the first character, however, to prevent accidental input
from causing mishaps:

    >>> utils.answers_for_testing = ['I reallY do not want it', 'n']
    >>> utils.ask('Does mocking work?', default=None)
    Question: Does mocking work? (y/n)?
    Our reply: I reallY do not want it
    Please explicitly answer y/n
    Question: Does mocking work? (y/n)?
    Our reply: n
    False

You can also ask for a version number. Pass it as the default value:

    >>> utils.answers_for_testing = ['']
    >>> utils.ask_version('New version', default='72.0')
    Question: New version [72.0]:
    Our reply: <ENTER>
    '72.0'
    >>> utils.answers_for_testing = ['1.0', '', '']
    >>> utils.ask_version('New version', default='72.0')
    Question: New version [72.0]:
    Our reply: 1.0
    '1.0'


Not asking input
----------------

For running automatically, the ``--no-input`` option is available. By default
it is off:

    >>> utils.AUTO_RESPONSE
    False

We can switch it on:

    >>> utils.TESTMODE = False
    >>> utils.answers_for_testing = []
    >>> import sys
    >>> sys.argv[1:] = ['--no-input']
    >>> utils.parse_options()
    >>> utils.AUTO_RESPONSE
    True

This way, answers aren't even asked. With a default of yes:

    >>> utils.answers_for_testing = ['']
    >>> utils.ask('Does mocking work?')
    True

A default of False:

    >>> utils.answers_for_testing = ['']
    >>> utils.ask('Does mocking work?', default=False)
    False

A default of None requires an answer, which means we cannot run in
``--no-input`` mode. Return an error in this case:

    >>> utils.ask('Does mocking work?', default=None)
    Traceback (most recent call last):
    ...
    RuntimeError: The question 'Does...' requires a manual answer...

The default for versions is accepted, too:

    >>> utils.answers_for_testing = []
    >>> utils.ask('What mocking version, my liege?', default='72')
    '72'


Output filtering
----------------

Uploading to pypi returns quite some lines of output.  Only show the last
part:

    >>> output = "a\nb\nc\nd\ne\nf\ng\n"
    >>> utils.show_last_lines(output)
    Showing last few lines...
    d
    e
    f
    g
    <BLANKLINE>

Or the first and last few lines, though we make sure we do not print
too much in case there are not 'enough' lines:

    >>> utils.show_first_and_last_lines(output)
    a
    b
    c
    d
    e
    f
    g
    <BLANKLINE>
    >>> import string
    >>> output = '\n'.join(string.ascii_lowercase)
    >>> utils.show_first_and_last_lines(output)
    Showing first few lines...
    a
    b
    c
    d
    e
    ...
    Showing last few lines...
    v
    w
    x
    y
    z

Just one line: no problem:

    >>> output = "just one line, no newlines"
    >>> utils.show_last_lines(output)
    Showing last few lines...
    just one line, no newlines
    >>> utils.show_first_and_last_lines(output)
    just one line, no newlines


Changelog header detection
--------------------------

Empty changelog:

    >>> lines = []
    >>> utils.extract_headings_from_history(lines)
    []

Various forms of version+date lines are recognised.  "unreleased" or a date in
paretheses:

    >>> lines = ["1.2 (unreleased)"]
    >>> pprint(utils.extract_headings_from_history(lines))
    [{'date': 'unreleased', 'line': 0, 'version': '1.2'}]
    >>> lines = ["1.1 (2008-12-25)"]
    >>> pprint(utils.extract_headings_from_history(lines))
    [{'date': '2008-12-25', 'line': 0, 'version': '1.1'}]

And dash-separated:

    >>> lines = ["1.0 - 1972-12-25"]
    >>> pprint(utils.extract_headings_from_history(lines))
    [{'date': '1972-12-25', 'line': 0, 'version': '1.0'}]

Versions with beta markers and spaces are fine:

    >>> lines = ["1.4 beta - unreleased"]
    >>> pprint(utils.extract_headings_from_history(lines))
    [{'date': 'unreleased', 'line': 0, 'version': '1.4 beta'}]

Multiple headers:

    >>> lines = ["1.2 (unreleased)",
    ...          "----------------",
    ...          "",
    ...          "- I did something.  [reinout]",
    ...          "",
    ...          "1.1 (2008-12-25)"
    ...          "----------------",
    ...          "",
    ...          "- Played Herodes in church play.  [reinout]",
    ...          ""]
    >>> pprint(utils.extract_headings_from_history(lines))
    [{'date': 'unreleased', 'line': 0, 'version': '1.2'},
     {'date': '2008-12-25', 'line': 5, 'version': '1.1'}]


reST headings
-------------

If a second line looks like a reST header line, fix up the length:

    >>> first = 'Hey, a potential heading'
    >>> second = '-------'
    >>> utils.fix_rst_heading(first, second)
    '------------------------'
    >>> second = '=='
    >>> utils.fix_rst_heading(first, second)
    '========================'
    >>> second = '``'
    >>> utils.fix_rst_heading(first, second)
    '````````````````````````'
    >>> second = '~~'
    >>> utils.fix_rst_heading(first, second)
    '~~~~~~~~~~~~~~~~~~~~~~~~'

No header line?  Just return the second line as-is:

    >>> second = 'just some text'
    >>> utils.fix_rst_heading(first, second)
    'just some text'

Empty line?  Just return it.

    >>> second = ''
    >>> utils.fix_rst_heading(first, second)
    ''

The second line must be uniform:

    >>> second = '- bullet point, no header'
    >>> utils.fix_rst_heading(first, second)
    '- bullet point, no header'


Safe setup.py running
---------------------

``setup_py()`` returns the ``python setup.py xyz`` command line by using
sys.executable.

    >>> cmd = utils.setup_py('cook a cow')
    >>> print cmd.replace(sys.executable, 'python') # test normalization
    python setup.py cook a cow

When the setup.py arguments include arguments that indicate pypi interaction,
the python executable is replaced by ``echo`` for safety reasons.  This only
happens when TESTMODE is on:

    >>> utils.TESTMODE = True
    >>> print utils.setup_py('upload')
    echo MOCK setup.py upload
    >>> print utils.setup_py('mupload')
    echo MOCK setup.py mupload
    >>> print utils.setup_py('register')
    echo MOCK setup.py register
    >>> print utils.setup_py('mregister')
    echo MOCK setup.py mregister


Data dict documentation
-----------------------

The releasers have a data dict that is passed to entry points (and used
internally).  Because of the entry points, good documentation is necessary.
So we can check whether all keys have attached documentation:

    >>> data = {'commit_msg': 'get me some booze'}
    >>> documentation = {'commit_msg': 'Commit message for svn',
    ...                  'target': 'Some other thingy'}
    >>> utils.is_data_documented(data, documentation)
    Checking data dict

We print a warning when something is undocumented:

    >>> data = {'version': '0.1'}
    >>> utils.is_data_documented(data, documentation)
    Checking data dict
    Internal detail: key(s) ['version'] are not documented


Entry-point-documentation-generation entry point
------------------------------------------------

The entry point for generating documentation does not run when the project
name isn't zest.releaser.  Otherwise we would generate our documentation every
time we used zest.releaser...

    >>> data = {'name': 'vanrees.worlddomination'}
    >>> utils.prepare_documentation_entrypoint(data) == None
    True

Prepare a mock documentation file:

    >>> import os
    >>> import shutil
    >>> import tempfile
    >>> tempdir = tempfile.mkdtemp()
    >>> mock_docdir = os.path.join(tempdir, 'doc', 'source')
    >>> os.makedirs(mock_docdir)
    >>> docfile = os.path.join(mock_docdir, 'entrypoints.rst')
    >>> open(docfile, 'wb').write("""
    ... line1
    ... line2
    ... .. ### AUTOGENERATED FROM HERE ###
    ... line3
    ... """)

When the name *is* zest.releaser, we generate documentation.

    >>> data = {'name': 'zest.releaser',
    ...         'workingdir': tempdir}
    >>> utils.prepare_documentation_entrypoint(data)
    Wrote entry point documentation to ...doc/source/entrypoints.rst

The lines above the marker interface are still intact, the line below it has
been replaced by the generated documentation:

    >>> print open(docfile).read()
    <BLANKLINE>
    line1
    line2
    .. ### AUTOGENERATED FROM HERE ###
    <BLANKLINE>
    Prerelease data dict items
    --------------------------
    <BLANKLINE>
    commit_msg
        Message template used when committing
    ...

Clean up

    >>> shutil.rmtree(tempdir)
