From c6aa039e5d0c721a7df109ca0daa06775e36d0f0 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:11 +0000
Subject: [PATCH 1/15] Generate documents by sphinx


Basic documents structure is added and APIs are also added as startup.

Fix #50

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/.gitignore b/.gitignore
index c80d326..2befce6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,5 @@ tags
 htmlcov/
 /.eggs/
 .env/
+
+doc/build/
diff --git a/MANIFEST.in b/MANIFEST.in
index b935e18..b9a6593 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,10 +3,10 @@ include COPYING-koji
 include LGPL
 include CHANGELOG.rst
 include README.rst
-include doc/rpkg_man_page.py
 include bin/rpkg
 recursive-include tests *
 recursive-include etc *
+recursive-include doc *.rst *.py
 
 include requirements/README.rst
 include requirements/*.txt
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..8663617
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    = -E
+SPHINXBUILD   = sphinx-build
+SPHINXPROJ    = rpkg
+SOURCEDIR     = source
+BUILDDIR      = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/source/api.rst b/doc/source/api.rst
new file mode 100644
index 0000000..3b03904
--- /dev/null
+++ b/doc/source/api.rst
@@ -0,0 +1,81 @@
+.. _api:
+
+API
+===
+
+Exceptions
+----------
+
+.. autoexception:: pyrpkg.errors.rpkgError
+.. autoexception:: pyrpkg.errors.rpkgAuthError
+.. autoexception:: pyrpkg.errors.UnknownTargetError
+.. autoexception:: pyrpkg.errors.HashtypeMixingError
+.. autoexception:: pyrpkg.errors.MalformedLineError
+.. autoexception:: pyrpkg.errors.InvalidHashType
+.. autoexception:: pyrpkg.errors.DownloadError
+.. autoexception:: pyrpkg.errors.UploadError
+
+
+cli
+---
+
+.. autoclass:: pyrpkg.cli.cliClient
+   :members:
+   :undoc-members:
+   :private-members:
+
+
+commands
+--------
+
+.. autoclass:: pyrpkg.__init__.NullHandler
+   :members:
+   :undoc-members:
+
+.. autoclass:: pyrpkg.__init__.Commands
+   :members:
+   :undoc-members:
+
+
+Lookaside
+---------
+
+.. autoclass:: pyrpkg.lookaside.CGILookasideCache
+   :members:
+   :undoc-members:
+
+
+Sources
+-------
+
+.. autoclass:: pyrpkg.sources.SourcesFile
+   :members:
+   :undoc-members:
+
+.. autoclass:: pyrpkg.sources.SourceFileEntry
+   :members:
+   :undoc-members:
+
+.. autoclass:: pyrpkg.sources.BSDSourceFileEntry
+   :members:
+   :undoc-members:
+
+
+gitignore
+---------
+
+.. autoclass:: pyrpkg.gitignore.GitIgnore
+   :members:
+   :undoc-members:
+
+
+Utilities
+---------
+
+.. autoclass:: pyrpkg.utils.cached_property
+   :members:
+   :undoc-members:
+
+.. autofunction:: pyrpkg.utils.warn_deprecated
+.. autofunction:: pyrpkg.utils._log_value
+.. autofunction:: pyrpkg.utils.log_result
\ No newline at end of file
diff --git a/doc/source/conf.py b/doc/source/conf.py
new file mode 100644
index 0000000..de50338
--- /dev/null
+++ b/doc/source/conf.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+#
+# rpkg documentation build configuration file, created by
+# sphinx-quickstart on Tue Dec 26 21:06:01 2017.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../..'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = ['sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'rpkg'
+copyright = u'2017, rpkg team'
+author = u'rpkg team'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = u'1.51'
+# The full version, including alpha/beta/rc tags.
+release = u'1.51'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = []
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# This is required for the alabaster theme
+# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
+html_sidebars = {
+    '**': [
+        'globaltoc.html',
+        'relations.html',  # needs 'show_related': True theme option to display
+        'searchbox.html',
+    ]
+}
+
+
+# -- Options for HTMLHelp output ------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'rpkgdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'rpkg.tex', u'rpkg Documentation',
+     u'rpkg team', 'manual'),
+]
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'rpkg', u'rpkg Documentation',
+     [author], 1)
+]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'rpkg', u'rpkg Documentation',
+     author, 'rpkg', 'One line description of project.',
+     'Miscellaneous'),
+]
+
+
+
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
new file mode 100644
index 0000000..382be14
--- /dev/null
+++ b/doc/source/configuration.rst
@@ -0,0 +1,5 @@
+.. _configuration:
+
+Configuration
+=============
+
diff --git a/doc/source/index.rst b/doc/source/index.rst
new file mode 100644
index 0000000..bf46bb0
--- /dev/null
+++ b/doc/source/index.rst
@@ -0,0 +1,23 @@
+.. rpkg documentation master file, created by
+   sphinx-quickstart on Tue Dec 26 21:06:01 2017.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to rpkg's documentation!
+================================
+
+.. toctree::
+   :maxdepth: 2
+
+   intro
+   configuration
+   api
+   migrations
+   release_notes
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/doc/source/intro.rst b/doc/source/intro.rst
new file mode 100644
index 0000000..d9b25df
--- /dev/null
+++ b/doc/source/intro.rst
@@ -0,0 +1,3 @@
+.. _intro:
+
+.. include:: ../../README.rst
\ No newline at end of file
diff --git a/doc/source/migrations.rst b/doc/source/migrations.rst
new file mode 100644
index 0000000..f420434
--- /dev/null
+++ b/doc/source/migrations.rst
@@ -0,0 +1,12 @@
+.. _migrations:
+
+Migration
+=========
+
+Here are migration guide to migrate existing code based on latest rpkg code
+base.
+
+.. toctree::
+   :maxdepth: 1
+
+   migrations/use-kojiprofile
diff --git a/doc/source/migrations/use-kojiprofile.rst b/doc/source/migrations/use-kojiprofile.rst
new file mode 100644
index 0000000..5ae11ed
--- /dev/null
+++ b/doc/source/migrations/use-kojiprofile.rst
@@ -0,0 +1,22 @@
+.. _use-kojiprofile:
+
+Use kojiprofile
+===============
+
+``kojiprofile`` is supported from version 1.50, and ``kojiconfig`` is
+deprecated at same time. To migrate to ``kojiprofile``, please follow these
+steps below.
+
+* Add ``kojiprofile`` in your application's configuration file. For example,
+
+  .. code-block:: ini
+
+    [myapp]
+    kojiprofile = koji
+
+* Remove deprecated ``kojiconfig`` from configuration file.
+
+* Replace argument ``kojiconfig`` with ``kojiprofile`` in ``Commands.__init__``.
+
+* Remove argument ``kojiconfig`` from ``Commands.container_build_koji``
+  argument list.
diff --git a/doc/source/release_notes.rst b/doc/source/release_notes.rst
new file mode 100644
index 0000000..e4b6d54
--- /dev/null
+++ b/doc/source/release_notes.rst
@@ -0,0 +1,10 @@
+.. _release_notes:
+
+Release Notes
+=============
+
+.. include release notes of each version from directory release_notes.
+
+.. toctree::
+   :maxdepth: 1
+
diff --git a/doc/use-kojiprofile.rst b/doc/use-kojiprofile.rst
deleted file mode 100644
index 38fae90..0000000
--- a/doc/use-kojiprofile.rst
+++ /dev/null
@@ -1,20 +0,0 @@
-Use kojiprofile
-===============
-
-``kojiprofile`` is supported from version 1.50, and ``kojiconfig`` is
-deprecated at same time. To migrate to ``kojiprofile``, please follow these
-steps below.
-
-* Add ``kojiprofile`` in your application's configuration file. For example,
-
-  ::
-
-    [myapp]
-    kojiprofile = koji
-
-* Remove deprecated ``kojiconfig`` from configuration file.
-
-* Replace argument ``kojiconfig`` with ``kojiprofile`` in ``Commands.__init__``.
-
-* Remove argument ``kojiconfig`` from ``Commands.container_build_koji``
-  argument list.

From daa3fb03cdc376a2bf5f60138c64f858cf84c286 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:11 +0000
Subject: [PATCH 2/15] Update existing docstrings


Update existing docstrings so that API documentation can be generated
in good format.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/source/api.rst b/doc/source/api.rst
index 3b03904..1042b23 100644
--- a/doc/source/api.rst
+++ b/doc/source/api.rst
@@ -35,6 +35,7 @@ commands
 .. autoclass:: pyrpkg.__init__.Commands
    :members:
    :undoc-members:
+   :private-members:
 
 
 Lookaside
diff --git a/pyrpkg/__init__.py b/pyrpkg/__init__.py
index 3c8f5b3..052b94a 100644
--- a/pyrpkg/__init__.py
+++ b/pyrpkg/__init__.py
@@ -196,12 +196,13 @@ class Commands(object):
     def lookasidecache(self):
         """A helper to interact with the lookaside cache
 
-        This is a pyrpkg.lookaside.CGILookasideCache instance, providing all
-        the needed stuff to communicate with a Fedora-style lookaside cache.
-
         Downstream users of the pyrpkg API may override this property with
         their own, returning their own implementation of a lookaside cache
         helper object.
+
+        :return: lookaside cache instance providing all the needed stuff to
+            communicate with a Fedora-style lookaside cache.
+        :rtype: :py:class:`pyrpkg.lookaside.CGILookasideCache`
         """
         return CGILookasideCache(
             self.lookasidehash, self.lookaside, self.lookaside_cgi,
@@ -397,11 +398,9 @@ class Commands(object):
     def load_branch_merge(self):
         """Find the remote tracking branch from the branch we're on.
 
-        The goal of this function is to catch if we are on a branch we
-
-        can make some assumptions about.  If there is no merge point
-
-        then we raise and ask the user to specify.
+        The goal of this function is to catch if we are on a branch we can make
+        some assumptions about. If there is no merge point then we raise and
+        ask the user to specify.
         """
 
         if self.dist:
@@ -922,28 +921,36 @@ class Commands(object):
     # Define some helper functions, they start with _
 
     def _has_krb_creds(self):
-        """Kerberos authentication is disabled if neither gssapi nor krbV is available"""
+        """
+        Kerberos authentication is disabled if neither gssapi nor krbV is
+        available
+        """
         return cccolutils.has_creds()
 
     def _run_command(self, cmd, shell=False, env=None, pipe=[], cwd=None):
         """Run the given command.
 
-        _run_command is able to run single command or two commands via pipe.
+        `_run_command` is able to run single command or two commands via pipe.
         Whatever the way to run the command, output to both stdout and stderr
         will not be captured and output to terminal directly, that is useful
         for caller to redirect.
 
-        cmd is a list of the command and arguments
-
-        shell is whether to run in a shell or not, defaults to False
-
-        env is a dict of environment variables to use (if any)
-
-        pipe is a command to pipe the output of cmd into
-
-        cwd is the optional directory to run the command from
-
-        Raises on error, or returns nothing.
+        :param cmd: executable and arguments to run. Depending on argument
+            `shell`, `cmd` could be a list if `shell` is `False`, and a string
+            if `False` is passed to `shell`.
+        :type cmd: str or list
+        :param bool shell: whether to run in a shell or not, defaults to
+            `False`.
+        :param env: environment variables to use (if any)
+        :type env: dict(str, str)
+        :param pipe: command to pipe the output of `cmd` into. Same as argument
+            `cmd`, `pipe` could be a string or list depending on value of
+            `shell`.
+        :type pipe: str or list
+        :param str cwd: optional directory to run the command from
+        :raises rpkgError: any error raised from underlying
+            `subprocess` while executing command in local system will be mapped
+            to :py:exc:`rpkgError`.
         """
 
         # Process any environment variables.
@@ -985,16 +992,14 @@ class Commands(object):
     def _newer(self, file1, file2):
         """Compare the last modification time of the given files
 
-        Returns True is file1 is newer than file2
-
+        :return: True if file1 is newer than file2
+        :rtype: bool
         """
 
         return os.path.getmtime(file1) > os.path.getmtime(file2)
 
     def _get_build_arches_from_spec(self):
-        """Given the path to an spec, retrieve the build arches
-
-        """
+        """Given the path to an spec, retrieve the build arches"""
 
         spec = os.path.join(self.path, self.spec)
         try:
@@ -1013,7 +1018,6 @@ class Commands(object):
         """Given the path to an srpm, determine the possible build arches
 
         Use supplied arches as a filter, only return compatible arches
-
         """
 
         archlist = arches
@@ -1064,7 +1068,11 @@ class Commands(object):
             self.repo.git.fetch(remote)
 
     def _list_branches(self, fetch=True):
-        """Returns a tuple of local and remote branch names"""
+        """Returns a tuple of local and remote branch names
+
+        :return: a pair of local branch names and remote branch names.
+        :rtype: tuple(list, list)
+        """
 
         if fetch:
             self._fetch_remotes()
@@ -1115,10 +1123,9 @@ class Commands(object):
     def _get_namespace_giturl(self, module):
         """Get the namespaced git url, if DistGit namespaces enabled
 
-        Takes a module name
-
-        Returns a string of giturl
-
+        :param str module: a module name
+        :return: giturl with proper namespace.
+        :rtype: str
         """
 
         if self.distgit_namespaced:
@@ -1138,10 +1145,9 @@ class Commands(object):
     def _get_namespace_anongiturl(self, module):
         """Get the namespaced git url, if DistGit namespaces enabled
 
-        Takes a module name
-
-        Returns a string of giturl
-
+        :param str module: a module name
+        :return: anonymous giturl with a proper namespace.
+        :rtype: str
         """
 
         if self.distgit_namespaced:
@@ -1158,13 +1164,10 @@ class Commands(object):
     def add_tag(self, tagname, force=False, message=None, file=None):
         """Add a git tag to the repository
 
-        Takes a tagname
-
-        Optionally can force the tag, include a message,
-        or reference a message file.
-
-        Runs the tag command and returns nothing
-
+        :param str tagname: tag name.
+        :param bool force: Optionally can force the tag, include a message, or
+            reference a message file.
+        :param str file: Optional. File containing tag message.
         """
 
         cmd = ['git', 'tag']
@@ -1185,12 +1188,9 @@ class Commands(object):
     def clean(self, dry=False, useignore=True):
         """Clean a module checkout of untracked files.
 
-        Can optionally perform a dry-run
-
-        Can optionally not use the ignore rules
-
-        Logs output and returns nothing
-
+        :param bool dry: Can optionally perform a dry-run. Defaults to `False`.
+        :param bool useignore: Can optionally not use the ignore rules.
+            Defaults to `True`.
         """
 
         # setup the command, this could probably be done with some python api...
@@ -1209,21 +1209,16 @@ class Commands(object):
               anon=False, target=None):
         """Clone a repo, optionally check out a specific branch.
 
-        module is the name of the module to clone
-
-        path is the basedir to perform the clone in
-
-        branch is the name of a branch to checkout instead of <remote>/master
-
-        bare_dir is the name of a directory to make a bare clone to, if this
-        is a bare clone. None otherwise.
-
-        anon is whether or not to clone anonymously
-
-        target is the name of the folder in which to clone the repo
-
-        Logs the output and returns nothing.
-
+        :param str module: the name of the module to clone
+        :param str path: an optional basedir to perform the clone in.
+        :param str branch: an optional name of a branch to checkout instead of
+            `<remote>/master`.
+        :param str bare_dir: an optional name of a directory to make a bare
+            clone to if this is a bare clone. `None` otherwise.
+        :param bool anon: whether or not to clone anonymously. Defaults to
+            `False`.
+        :param str target: an optional name of the folder in which to clone the
+            repo.
         """
 
         if not path:
@@ -1285,10 +1280,11 @@ class Commands(object):
     def clone_with_dirs(self, module, anon=False, target=None):
         """Clone a repo old style with subdirs for each branch.
 
-        module is the name of the module to clone
-
-        gitargs is an option list of arguments to git clone
-
+        :param str module: name of the module to clone
+        :param bool anon: whether or not to clone anonymously. Defaults to
+            `False`.
+        :param str target: an optional name of the folder in which to clone the
+            repo.
         """
 
         self._push_url = None
@@ -1362,16 +1358,12 @@ class Commands(object):
     def commit(self, message=None, file=None, files=[], signoff=False):
         """Commit changes to a module (optionally found at path)
 
-        Can take a message to use as the commit message
-
-        a file to find the commit message within
-
-        and a list of files to commit.
-
         Requires the caller be a real tty or a message passed.
 
-        Logs the output and returns nothing.
-
+        :param str message: an optional message to use as the commit message
+        :param str file: an optional file to find the commit message within
+        :param list files: an optional list to list files to commit.
+        :param bool signoff: signoff commit optionally. Defaults to `False`.
         """
 
         # First lets see if we got a message or we're on a real tty:
@@ -1416,13 +1408,10 @@ class Commands(object):
     def diff(self, cached=False, files=[]):
         """Execute a git diff
 
-        optionally diff the cached or staged changes
-
-        Takes an optional list of files to diff relative to the module base
-        directory
-
-        Logs the output and returns nothing
-
+        :param bool cached: optionally diff the cached or staged changes.
+            Defaults to not.
+        :param list files: optional list of files to diff relative to the
+            module base directory.
         """
 
         # Things work better if we're in our module directory
@@ -1510,14 +1499,12 @@ class Commands(object):
     def import_srpm(self, srpm):
         """Import the contents of an srpm into a repo.
 
-        srpm: File to import contents from
-
         This function will add/remove content to match the srpm,
-
         upload new files to the lookaside, and stage the changes.
 
-        Returns a list of files to upload.
-
+        :param str srpm: file to import contents from.
+        :return: a list of files to upload.
+        :rtype: list
         """
         # bail if we're dirty
         if self.repo.is_dirty():
@@ -1584,9 +1571,8 @@ class Commands(object):
     def list_tag(self, tagname='*'):
         """List all tags in the repository which match a given tagname.
 
-        The optional `tagname` argument may be a shell glob (it is matched
-        with fnmatch).
-
+        :param str tagname: an optional shell glob to match part of tags (it
+            is matched with `fnmatch`). Defaults to '`*`' to list all tags.
         """
         if tagname is None:
             tagname = '*'
@@ -2200,9 +2186,15 @@ class Commands(object):
         Takes localargs (passed to rpmbuild), arch to build for, and hashtype
         to build with.
 
-        Writes output to a log file and logs it to the logger
+        Writes output to a log file and logs it to the logger. Log file is
+        written into current working directory and in format
+        `.build-{version}-{release}.log`.
 
-        Returns the returncode from the build call
+        :param str arch: to optionally build for a specific arch.
+        :param str hashtype: an alternative algorithm used for payload file
+            digests.
+        :param str builddir: an alternative builddir.
+        :raises rpkgError: if underlying `rpmbuild` fails.
         """
 
         # This could really use a list of arches to build for and loop over
@@ -2245,8 +2237,12 @@ class Commands(object):
     def mock_config(self, target=None, arch=None):
         """Generate a mock config based on branch data.
 
-        Can use option target and arch to override autodiscovery.
-        Will return the mock config file text.
+        :param str target: an alternative build target, otherwise default build
+            target will be used.
+        :param str arch: an alternative arch, otherwise local system arch will
+            be used.
+        :return: the mock config content got from Koji.
+        :rtype: str
         """
 
         # Figure out some things about ourself.
@@ -2283,7 +2279,8 @@ class Commands(object):
 
         If files are found in system config directory for mock they are copied
         to mock config directory defined as method's argument. Otherwise empty
-        files are created."""
+        files are created.
+        """
         for filename in filenames:
             system_filename = '/etc/mock/%s' % filename
             tmp_filename = os.path.join(config_dir, filename)
@@ -2309,7 +2306,8 @@ class Commands(object):
         processing, temporary directory is removed. Otherwise it caller's
         responsibility to remove this directory.
 
-        Returns used config directory"""
+        Returns used config directory
+        """
         if not root:
             root = self.mockconfig
         if not config_dir:
@@ -2401,7 +2399,13 @@ class Commands(object):
     def upload(self, files, replace=False):
         """Upload source file(s) in the lookaside cache
 
-        Can optionally replace the existing tracked sources
+        Both file `sources` and `.gitignore` will be updated with uploaded
+        files, and added to index tree eventually.
+
+        :param iterable files: an iterable of files to upload.
+        :param bool replace: optionally replace the existing tracked sources.
+            Defaults to `False`.
+        :raises rpkgError: if failed to add a file to file `sources`.
         """
 
         sourcesf = SourcesFile(self.sources_filename, self.source_entry_type,
@@ -2441,12 +2445,11 @@ class Commands(object):
         self.repo.index.add(['sources', '.gitignore'])
 
     def prep(self, arch=None, builddir=None):
-        """Run rpm -bp on a module
-
-        optionally for a specific arch, or
-        define an alternative builddir
+        """Run `rpmbuild -bp` on a module
 
-        Logs the output and returns nothing
+        :param str arch: optional to run prep section for a specific arch. By
+            default, local system arch will be used.
+        :param str builddir: an alternative builddir.
         """
 
         # setup the rpm command
@@ -2466,7 +2469,11 @@ class Commands(object):
     def srpm(self, hashtype=None):
         """Create an srpm using hashtype from content in the module
 
-        Requires sources already downloaded.
+        Requires sources already downloaded. The generated SRPM file will be
+        put into package repository directory.
+
+        :param str hashtype: an alternative algorithm used for payload file
+            digests.
         """
 
         self.srpmname = os.path.join(self.path,
@@ -2497,7 +2504,8 @@ class Commands(object):
     def unused_patches(self):
         """Discover patches checked into source control that are not used
 
-        Returns a list of unused patches, which may be empty.
+        :return: a list of unused patches, which may be empty.
+        :rtype: list
         """
 
         # Create a list for unused patches
@@ -2533,9 +2541,11 @@ class Commands(object):
 
     def _byte_offset_to_line_number(self, text, offset):
         """
-        Convert byte offset (given by e.g. DecodeError) to human readable
+        Convert byte offset (given by e.g. `DecodeError`) to human readable
         format (line number and char position)
-        Return a list with line number and char offset
+
+        :return: a pair of line number and char offset.
+        :rtype: list(int, int)
         """
         offset_inc = 0
         line_num = 1
@@ -2548,9 +2558,9 @@ class Commands(object):
         return [line_num, offset - offset_inc + 1]
 
     def verify_files(self, builddir=None):
-        """Run rpmbuild -bl on a module to verify the %files section
+        """Run `rpmbuild -bl` on a module to verify the `%files` section
 
-        optionally define an alternate builddir
+        :param str builddir: optionally define an alternate builddir.
         """
 
         # setup the rpm command
@@ -2683,20 +2693,19 @@ class Commands(object):
                             oidc_client_secret=None, oidc_scopes=None):
         """
         Cancel an MBS build
-        :param api_url: a string of the URL of the MBS API
-        :param build_id: an integer of the build ID to cancel
-        :param auth_method: a string of the authentication method used by the
-        MBS
-        :kwarg oidc_id_provider: a string of the OIDC provider when MBS is
-        using OIDC for authentication
-        :kwarg oidc_client_id: a string of the OIDC client ID when MBS is
-        using OIDC for authentication
-        :kwarg oidc_client_secret: a string of the OIDC client secret when MBS
-        is using OIDC for authentication. Based on the OIDC setup, this could
-        be None.
-        :kwarg oidc_scopes: a list of OIDC scopes when MBS is using OIDC for
-        authentication
-        :return: None
+
+        :param str api_url: URL of the MBS API
+        :param int build_id: build ID to cancel
+        :param str auth_method: authentication method used by the MBS
+        :kwarg str oidc_id_provider: the OIDC provider when MBS is using OIDC
+            for authentication
+        :param str oidc_client_id: the OIDC client ID when MBS is using OIDC
+            for authentication
+        :param str oidc_client_secret: the OIDC client secret when MBS is using
+            OIDC for authentication. Based on the OIDC setup, this could be
+            `None`.
+        :param list oidc_scopes: a list of OIDC scopes when MBS is using OIDC
+            for authentication
         """
         # Make sure the build they are trying to cancel exists
         self.module_get_build(api_url, build_id)
@@ -2716,9 +2725,9 @@ class Commands(object):
     def module_build_info(self, api_url, build_id):
         """
         Show information about an MBS build
-        :param api_url: a string of the URL of the MBS API
-        :param build_id: an integer of the build ID to query MBS about
-        :return: None
+
+        :param str api_url: URL of the MBS API
+        :param int build_id: build ID to query MBS about
         """
         # Load the Koji session anonymously so we get access to the Koji web
         # URL
@@ -2769,13 +2778,15 @@ class Commands(object):
     def module_get_url(self, api_url, build_id, action='GET'):
         """
         Get the proper MBS API URL for the desired action
-        :param api_url: a string of the URL of the MBS API
-        :param build_id: an integer of the module build desired. If this is set
-        to None, then the base URL for all module builds is returned.
-        :kwarg action: a string determining the HTTP action. If this is set to
-        GET, then the URL will contain `?verbose=true`. Any other value will
-        not have verbose set.
-        :return: a string of the desired MBS API URL
+
+        :param str api_url: a string of the URL of the MBS API
+        :param int build_id: an integer of the module build desired. If this is
+            set to None, then the base URL for all module builds is returned.
+        :param str action: a string determining the HTTP action. If this is set
+            to `GET`, then the URL will contain `?verbose=true`. Any other
+            value will not have verbose set.
+        :return: a string of the desired MBS API URL.
+        :rtype: str
         """
         url = urljoin(api_url, 'module-builds/')
         if build_id is not None:
@@ -2791,8 +2802,10 @@ class Commands(object):
     def module_get_koji_state_dict():
         """
         Get a dictionary of Koji build states with the keys being strings and
-        the values being their associated integer
+        the values being their associated integer.
+
         :return: a dictionary of Koji build states
+        :rtype: dict
         """
         state_names = dict([(v, k) for k, v in koji.BUILD_STATES.items()])
         state_names[None] = 'undefined'
@@ -2803,12 +2816,14 @@ class Commands(object):
         Determines the proper SCM URL and branch based on the arguments. If the
         user doesn't specify an SCM URL and branch, then the git repo the user
         is currently in is used instead.
-        :kwarg scm_url: a string of the module's SCM URL
-        :kwarg branch: a string of the module's branch
-        :kwarg check_repo: a boolean that determines if check_repo should be
-        run when an scm_url is not provided.
+
+        :param str scm_url: a string of the module's SCM URL
+        :param str branch: a string of the module's branch
+        :kwarg bool check_repo: a boolean that determines if check_repo should
+            be run when an scm_url is not provided.
         :return: a tuple containing a string of the SCM URL and a string of the
-        branch
+            branch
+        :rtype: tuple
         """
         if not scm_url and check_repo:
             # Make sure the local repo is clean (no unpushed changes) if the
@@ -2842,19 +2857,25 @@ class Commands(object):
                            mbs_config_section=None, default_streams=None):
         """
         A wrapper for `mbs-manager build_module_locally`.
-        :param file_path: a string, path of the module's modulemd yaml file.
-        :param stream: a string, stream of the module.
-        :kwarg local_builds_nsvs: a list of localbuild ids to import into MBS
-        before running this local build.
-        :kwarg verbose: a boolean specifying if mbs-manager should be verbose.
-        This is overridden by self.quiet.
-        :kwarg debug: a boolean specifying if mbs-manager should be debug.
-        This is overridden by self.quiet and verbose.
-        :kwarg skip_tests: a boolean determining if the check sections should be skipped
-        :kwarg mbs_config: a string, path to alternative MBS config file to use.
-        :kwarg mbs_config_section: a string, name of alternative config section to use.
-        :kwargs default_streams: a list, contains strings with default name:stream pairs
-        which are passed to mbs-manager using the '-s' command line argument.
+
+        :param str file_path: a string, path of the module's modulemd yaml file.
+        :param str stream: a string, stream of the module.
+        :param list local_builds_nsvs: a list of localbuild ids to import into
+            MBS before running this local build.
+        :param bool verbose: a boolean specifying if mbs-manager should be
+            verbose. This is overridden by self.quiet.
+        :param bool debug: a boolean specifying if mbs-manager should be debug.
+            This is overridden by self.quiet and verbose.
+        :param bool skip_tests: a boolean determining if the check sections
+            should be skipped.
+        :param str mbs_config: a string, path to alternative MBS config file to
+            use.
+        :param str mbs_config_section: a string, name of alternative config
+            section to use.
+        :param default_streams: a list, contains strings with default
+            `name:stream` pairs which are passed to mbs-manager using the '-s'
+            command line argument.
+        :type default_streams: list[str]
         :return: None
         """
         command = ['mbs-manager']
@@ -2898,12 +2919,12 @@ class Commands(object):
     def module_overview(self, api_url, limit=10, finished=True):
         """
         Show the overview of the latest builds in MBS
-        :param api_url: a string of the URL of the MBS API
-        :kwarg limit: an integer of the number of most recent module builds to
-        display. This defaults to 10.
-        :kwarg finished: a boolean that determines if only finished or
-        unfinished module builds should be displayed. This defaults to True.
-        :return: None
+
+        :param str api_url: a string of the URL of the MBS API
+        :param int limit: an integer of the number of most recent module builds
+            to display. This defaults to 10.
+        :param bool finished: a boolean that determines if only finished or
+            unfinished module builds should be displayed. This defaults to True.
         """
         # Don't let the user cause problems by specifying a negative limit
         if limit < 1:
@@ -2930,8 +2951,9 @@ class Commands(object):
             """
             Private function that is used for multithreading later on to get
             the desired amount of builds for a specific state.
+
             :param state: an integer representing the build state to query for
-            :return: yields dictionaries of the builds found
+            :return: a generator to yield dictionaries of the builds found
             """
             total = 0
             page = 1
@@ -3006,21 +3028,23 @@ class Commands(object):
                                        oidc_scopes=None, **kwargs):
         """
         Sends authorized request to MBS
-        :param verb: a string of the HTTP verb of the request (e.g. POST)
-        :param url: a string of the URL to make the request on
-        :param body: a dictionary of the data to send in the authorized request
-        :param auth_method: a string of the authentication method used by the
-        MBS
-        :kwarg oidc_id_provider: a string of the OIDC provider when MBS is
-        using OIDC for authentication
-        :kwarg oidc_client_id: a string of the OIDC client ID when MBS is
-        using OIDC for authentication
-        :kwarg oidc_client_secret: a string of the OIDC client secret when MBS
-        is using OIDC for authentication. Based on the OIDC setup, this could
-        be None.
-        :kwarg oidc_scopes: a list of OIDC scopes when MBS is using OIDC for
-        authentication
-        :kwarg **kwargs: any additional python-requests keyword arguments
+
+        :param str verb: a string of the HTTP verb of the request (e.g. `POST`)
+        :param str url: a string of the URL to make the request on.
+        :param dict body: a dictionary of the data to send in the authorized
+            request.
+        :param str auth_method: a string of the authentication method used by
+            the MBS. Valid methods are `oidc` and `kerberos`.
+        :param str oidc_id_provider: a string of the OIDC provider when MBS is
+            using OIDC for authentication
+        :param str oidc_client_id: a string of the OIDC client ID when MBS is
+            using OIDC for authentication
+        :param str oidc_client_secret: a string of the OIDC client secret when
+            MBS is using OIDC for authentication. Based on the OIDC setup, this
+            could be None.
+        :param list oidc_scopes: a list of OIDC scopes when MBS is using OIDC
+            for authentication
+        :param kwargs: any additional python-requests keyword arguments.
         :return: a python-requests response object
         """
         if auth_method == 'oidc':
@@ -3065,24 +3089,26 @@ class Commands(object):
                             oidc_scopes=None):
         """
         Submit a module build to the MBS
+
         :param api_url: a string of the URL of the MBS API
         :param scm_url: a string of the module's SCM URL
         :param branch: a string of the module's branch
-        :param auth_method: a string of the authentication method used by the
-        MBS
+        :param str auth_method: a string of the authentication method used by
+            the MBS.
         :param optional: an optional list of "key=value" to be passed in with
-        the MBS build submission
-        :kwarg oidc_id_provider: a string of the OIDC provider when MBS is
-        using OIDC for authentication
-        :kwarg oidc_client_id: a string of the OIDC client ID when MBS is
-        using OIDC for authentication
-        :kwarg oidc_client_secret: a string of the OIDC client secret when MBS
-        is using OIDC for authentication. Based on the OIDC setup, this could
-        be None.
-        :kwarg oidc_scopes: a list of OIDC scopes when MBS is using OIDC for
-        authentication
+            the MBS build submission.
+        :type optional: list[str]
+        :param str oidc_id_provider: a string of the OIDC provider when MBS is
+            using OIDC for authentication.
+        :param str oidc_client_id: a string of the OIDC client ID when MBS is
+            using OIDC for authentication
+        :param str oidc_client_secret: a string of the OIDC client secret when
+            MBS is using OIDC for authentication. Based on the OIDC setup, this
+            could be None. :kwarg oidc_scopes: a list of OIDC scopes when MBS
+            is using OIDC for authentication.
         :return: a list of module build IDs that are being built from this
-        request
+            request.
+        :rtype: list[int]
         """
         body = {'scmurl': scm_url, 'branch': branch}
         optional = optional if optional else []
@@ -3117,12 +3143,13 @@ class Commands(object):
 
     def module_watch_build(self, api_url, build_ids):
         """
-        Watches the first MBS build in the list in a loop that updates every
-        15 seconds. The loop ends when the build state is 'failed', 'done', or
+        Watches the first MBS build in the list in a loop that updates every 15
+        seconds. The loop ends when the build state is 'failed', 'done', or
         'ready'.
-        :param api_url: a string of the URL of the MBS API
+
+        :param str api_url: a string of the URL of the MBS API
         :param build_ids: a list of module build IDs
-        :return: None
+        :type build_ids: list[int]
         """
         build_id = build_ids[0]
         warning = None
diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py
index ac0d8fc..a952dc5 100644
--- a/pyrpkg/cli.py
+++ b/pyrpkg/cli.py
@@ -280,7 +280,9 @@ class cliClient(object):
 
         As a side effect method sets self.site with a loaded library.
 
-        site option can be used to specify which library to load
+        :param site: used to specify which library to load.
+        :type site: the module of downstream client that is built on top of
+            rpkg.
         """
 
         # We do some imports here to be more flexible
@@ -1705,11 +1707,9 @@ see API KEY section of copr-cli(1) man page.
         print(self.cmd.mock_config(self.args.target, self.args.arch))
 
     def module_build(self):
-        """
-        Builds a module using MBS
-        :return: None
-        """
+        """Builds a module using MBS"""
         api_url = self.module_api_url
+        self.module_validate_config()
         scm_url, branch = self.cmd.module_get_scm_info(
             self.args.scm_url, self.args.branch)
         auth_method, oidc_id_provider, oidc_client_id, oidc_client_secret, \
@@ -1732,11 +1732,9 @@ see API KEY section of copr-cli(1) man page.
             print('The {0} submitted to the MBS' .format(ids_to_print))
 
     def module_build_cancel(self):
-        """
-        Cancel an MBS build
-        :return: None
-        """
+        """Cancel an MBS build"""
         api_url = self.module_api_url
+        self.module_validate_config()
         build_id = self.args.build_id
         auth_method, oidc_id_provider, oidc_client_id, oidc_client_secret, \
             oidc_scopes = self.module_get_auth_config()
@@ -1750,17 +1748,14 @@ see API KEY section of copr-cli(1) man page.
                 print('The module build #{0} was cancelled'.format(build_id))
 
     def module_build_info(self):
-        """
-        Show information about an MBS build
-        :return: None
-        """
+        """Show information about an MBS build"""
         self.cmd.module_build_info(self.module_api_url, self.args.build_id)
+        self.module_validate_config()
+        api_url = self.config.get(self.config_section, 'api_url')
+        self.cmd.module_build_info(api_url, self.args.build_id)
 
     def module_build_local(self):
-        """
-        Build a module locally using mbs-manager
-        :return: None
-        """
+        """Build a module locally using mbs-manager"""
         self.module_validate_config()
 
         if not self.args.stream:
@@ -1791,12 +1786,13 @@ see API KEY section of copr-cli(1) man page.
             default_streams=self.args.default_streams)
 
     def module_get_auth_config(self):
-        """
-        Get the authentication configuration for the MBS
+        """Get the authentication configuration for the MBS
+
         :return: a tuple consisting of the authentication method, the OIDC ID
-        provider, the OIDC client ID, the OIDC client secret, and the OIDC
-        scopes. If the authentication method is not OIDC, the OIDC values in
-        the tuple are set to None.
+            provider, the OIDC client ID, the OIDC client secret, and the OIDC
+            scopes. If the authentication method is not OIDC, the OIDC values
+            in the tuple are set to None.
+        :rtype: tuple
         """
         auth_method = self.config.get(self.config_section, 'auth_method')
         oidc_id_provider = None
@@ -1865,26 +1861,18 @@ see API KEY section of copr-cli(1) man page.
                 api_url.rstrip('/'), api_version)
         return self._module_api_url
 
-    def module_build_watch(self):
-        """
-        Watch an MBS build from the command-line
-        :return: None
-        """
-        self.module_watch_build([self.args.build_id])
-
     def module_overview(self):
-        """
-        Show the overview of the latest builds in the MBS
-        :return: None
-        """
+        """Show the overview of the latest builds in the MBS"""
+        self.module_validate_config()
+        api_url = self.config.get(self.config_section, 'api_url')
         self.cmd.module_overview(
             self.module_api_url, self.args.limit,
             finished=(not self.args.unfinished))
 
     def module_validate_config(self):
-        """
-        Validates the configuration needed for MBS commands
-        :return: None or rpkgError
+        """Validates the configuration needed for MBS commands
+
+        :raises: :py:exc:`pyrpkg.errors.rpkgError`
         """
         self.config_section = '{0}.mbs'.format(self.name)
         # Verify that all necessary config options are set
@@ -1929,8 +1917,9 @@ see API KEY section of copr-cli(1) man page.
         Watches the first MBS build in the list in a loop that updates every
         15 seconds. The loop ends when the build state is 'failed', 'done', or
         'ready'.
+
         :param build_ids: a list of module build IDs
-        :return: None
+        :type build_ids: list[int]
         """
         self.cmd.module_watch_build(self.module_api_url, build_ids)
 
diff --git a/pyrpkg/gitignore.py b/pyrpkg/gitignore.py
index db3f3a5..c81da3e 100644
--- a/pyrpkg/gitignore.py
+++ b/pyrpkg/gitignore.py
@@ -16,13 +16,13 @@ import os
 
 class GitIgnore(object):
     """A class to manage a .gitignore file"""
+
     def __init__(self, path):
         """Constructor
 
-        Args:
-            path (str): The full path to the .gitignore file. If it does not
-                exist, the file will be created when running GitIgnore.write()
-                for the first time.
+        :param str path: The full path to the .gitignore file. If it does not
+            exist, the file will be created when running :py:meth:GitIgnore.write for
+            the first time.
         """
         self.path = path
 
@@ -45,9 +45,8 @@ class GitIgnore(object):
     def add(self, line):
         """Add a line
 
-        Args:
-            line (str): The line to add to the file. It will not be added if
-                it already matches an existing line.
+        :param str line: The line to add to the file. It will not be added if
+            it already matches an existing line.
         """
         if self.match(line):
             return
@@ -61,11 +60,9 @@ class GitIgnore(object):
 
         This uses fnmatch to match against wildcards.
 
-        Args:
-            line (str): The new line to match against existing ones.
-
-        Returns:
-            True if the new line matches, False otherwise.
+        :param str line: The new line to match against existing ones.
+        :return: True if the new line matches, False otherwise.
+        :rtype: bool
         """
         line = line.lstrip('/').rstrip('\n')
 
diff --git a/pyrpkg/lookaside.py b/pyrpkg/lookaside.py
index 5ced6e3..9282618 100644
--- a/pyrpkg/lookaside.py
+++ b/pyrpkg/lookaside.py
@@ -33,18 +33,17 @@ class CGILookasideCache(object):
                  client_cert=None, ca_cert=None):
         """Constructor
 
-        Args:
-            hashtype (str): The hash algorithm to use for uploads. (e.g 'md5')
-            download_url (str): The URL used to download source files.
-            upload_url (str): The URL of the CGI script called when uploading
-                source files.
-            client_cert (str, optional): The full path to the client-side
-                certificate to use for HTTPS authentication. It defaults to
-                None, in which case no client-side certificate is used.
-            ca_cert (str, optional): The full path to the CA certificate to
-                use for HTTPS connexions. (e.g if the server certificate is
-                self-signed. It defaults to None, in which case the system CA
-                bundle is used.
+        :param str hashtype: The hash algorithm to use for uploads. (e.g 'md5')
+        :param str download_url: The URL used to download source files.
+        :param str upload_url: The URL of the CGI script called when uploading
+            source files.
+        :param str client_cert: Optional. The full path to the client-side
+            certificate to use for HTTPS authentication. It defaults to None,
+            in which case no client-side certificate is used.
+        :param str ca_cert: Optional. The full path to the CA certificate to
+            use for HTTPS connexions. (e.g if the server certificate is
+            self-signed. It defaults to None, in which case the system CA
+            bundle is used.
         """
         self.hashtype = hashtype
         self.download_url = download_url
@@ -82,13 +81,10 @@ class CGILookasideCache(object):
     def hash_file(self, filename, hashtype=None):
         """Compute the hash of a file
 
-        Args:
-            filename (str): The full path to the file. It is assumed to exist.
-            hashtype (str, optional): The hash algorithm to use. (e.g 'md5')
-                This defaults to the hashtype passed to the constructor.
-
-        Returns:
-            The hash digest.
+        :param str filename: The full path to the file. It is assumed to exist.
+        :param str hashtype: Optional. The hash algorithm to use. (e.g 'md5')
+            This defaults to the hashtype passed to the constructor.
+        :return: The hash digest.
         """
         if hashtype is None:
             hashtype = self.hashtype
@@ -111,14 +107,12 @@ class CGILookasideCache(object):
     def file_is_valid(self, filename, hash, hashtype=None):
         """Ensure the file is correct
 
-        Args:
-            filename (str): The full path to the file. It is assumed to exist.
-            hash (str): The known good hash of the file.
-            hashtype (str, optional): The hash algorithm to use. (e.g 'md5')
-                This defaults to the hashtype passed to the constructor.
-
-        Returns:
-            True if the file is valid, False otherwise.
+        :param str filename: The full path to the file. It is assumed to exist.
+        :param str hash: The known good hash of the file.
+        :param str hashtype: Optional. The hash algorithm to use. (e.g 'md5')
+            This defaults to the hashtype passed to the constructor.
+        :return: True if the file is valid, False otherwise.
+        :rtype: bool
         """
         sum = self.hash_file(filename, hashtype)
         return sum == hash
@@ -142,17 +136,16 @@ class CGILookasideCache(object):
     def download(self, name, filename, hash, outfile, hashtype=None, **kwargs):
         """Download a source file
 
-        Args:
-            name (str): The name of the module. (usually the name of the SRPM).
-                        This can include the namespace as well (depending on
-                        what the server side expects).
-            filename (str): The name of the file to download.
-            hash (str): The known good hash of the file.
-            outfile (str): The full path where to save the downloaded file.
-            hashtype (str, optional): The hash algorithm. (e.g 'md5')
-                This defaults to the hashtype passed to the constructor.
-            **kwargs: Additional keyword arguments. They will be used when
-                constructing the full URL to the file to download.
+        :param str name: The name of the module. (usually the name of the
+            SRPM). This can include the namespace as well (depending on what
+            the server side expects).
+        :param str filename: The name of the file to download.
+        :param str hash: The known good hash of the file.
+        :param str outfile: The full path where to save the downloaded file.
+        :param str hashtype: Optional. The hash algorithm. (e.g 'md5') This
+            defaults to the hashtype passed to the constructor.
+        :param kwargs: Additional keyword arguments. They will be used when
+            constructing the full URL to the file to download.
         """
         if hashtype is None:
             hashtype = self.hashtype
@@ -207,12 +200,11 @@ class CGILookasideCache(object):
     def remote_file_exists(self, name, filename, hash):
         """Verify whether a file exists on the lookaside cache
 
-        Args:
-            name: The name of the module. (usually the name of the SRPM).
-                  This can include the namespace as well (depending on
-                  what the server side expects).
-            filename: The name of the file to check for.
-            hash: The known good hash of the file.
+        :param str name: The name of the module. (usually the name of the
+            SRPM). This can include the namespace as well (depending on what
+            the server side expects).
+        :param str filename: The name of the file to check for.
+        :param str hash: The known good hash of the file.
         """
 
         # RHEL 7 ships pycurl that does not accept unicode. When given unicode
@@ -279,12 +271,11 @@ class CGILookasideCache(object):
     def upload(self, name, filepath, hash):
         """Upload a source file
 
-        Args:
-            name (str): The name of the module. (usually the name of the SRPM)
-                        This can include the namespace as well (depending on
-                        what the server side expects).
-            filepath (str): The full path to the file to upload.
-            hash (str): The known good hash of the file.
+        :param str name: The name of the module. (usually the name of the SRPM)
+            This can include the namespace as well (depending on what the
+            server side expects).
+        :param str filepath: The full path to the file to upload.
+        :param str hash: The known good hash of the file.
         """
         filename = os.path.basename(filepath)
 
diff --git a/pyrpkg/utils.py b/pyrpkg/utils.py
index ab78cf7..2ce469f 100644
--- a/pyrpkg/utils.py
+++ b/pyrpkg/utils.py
@@ -63,12 +63,11 @@ class cached_property(property):
 def warn_deprecated(clsname, oldname, newname):
     """Emit a deprecation warning
 
-    Args:
-        clsname (str): The name of the class which has its attribute
-            deprecated.
-        oldname (str): The name of the deprecated attribute.
-        newname (str): The name of the new attribute, which should be used
-            instead.
+    :param str clsname: The name of the class which has its attribute
+        deprecated.
+    :param str oldname: The name of the deprecated attribute.
+    :param str newname: The name of the new attribute, which should be used
+        instead.
     """
     sys.stderr.write(
         "DeprecationWarning: %s.%s is deprecated and will be removed eventually.\n"

From fd715c7685f864f3584880c89f437e1ac8a38096 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 3/15] Generate commands HTML and man pages


Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/.gitignore b/.gitignore
index 2befce6..9e26969 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,5 @@ htmlcov/
 .env/
 
 doc/build/
+doc/source/commands/
+doc/source/man_pages.json
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
index b9a6593..74b1109 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,7 +6,9 @@ include README.rst
 include bin/rpkg
 recursive-include tests *
 recursive-include etc *
+
 recursive-include doc *.rst *.py
+prune doc/source/commands/
 
 include requirements/README.rst
 include requirements/*.txt
diff --git a/doc/generate_man_pages.py b/doc/generate_man_pages.py
new file mode 100644
index 0000000..1952da5
--- /dev/null
+++ b/doc/generate_man_pages.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+# generate_man_pages.py - script to generate documents for commands
+#
+# Copyright (C) 2017 Red Hat Inc.
+# Author(s): Chenxiong Qi <cqi@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
+# the full text of the license.
+
+from __future__ import print_function
+
+import argparse
+import json
+import logging
+import os
+import re
+import six
+import sys
+
+logging.basicConfig(level=logging.CRITICAL)
+logger = logging.getLogger('Generator')
+
+join = os.path.join
+
+project_root = os.path.abspath(join(os.path.dirname(__file__), '..'))
+sys.path.insert(0, project_root)
+
+doc_dir = join(project_root, 'doc')
+commands_docs_dir = join(doc_dir, 'source', 'commands')
+man_pages_json_file = join(doc_dir, 'source', 'man_pages.json')
+
+if not os.path.exists(commands_docs_dir):
+    os.mkdir(commands_docs_dir)
+
+command_rst_template = '''\
+.. _{name}:
+
+{name}
+{name_section_marker}
+
+.. program:: {name}
+
+Synopsis
+--------
+
+::
+
+{usage}
+
+Description
+-----------
+
+{description}
+
+{arguments}
+
+Options
+-------
+
+{options}
+'''
+
+command_option_template = '''\
+.. option:: {name_metavar}
+
+   {description}
+'''
+
+command_arguments_template = '''\
+Arguments
+---------
+
+{arguments}
+'''
+
+
+def format_literal(s):
+    """Format possible inline literal text
+
+    For example, if string contains text --some-option, it will be formatted
+    to ``--some-option``.
+    """
+    regex = '(--[a-z]+(-[a-z]+)+)'
+    return re.sub(regex, r'``\1``', s)
+
+
+def generate_man_pages_list_file(cli_subparsers):
+    """Generate registered commands list for generating man pages
+
+    The generated content will be written into a JSON file. Each command will
+    be mapped to man page information format that sphinx requires, which is
+
+    (source start file, name, description, authors, manual section)
+
+    But, we don't write the forth column authors, that will be handled
+    (inserted) by the piece of code inside `conf.py`.
+
+    So, the final JSON file will contain data dumped from this structure
+
+    [
+        ('commands/{command name}', '{command name}, 'description', 1),
+        ...
+    ]
+    """
+    action_helps = dict((
+        (action.dest, action.help)
+        for action in cli_subparsers._get_subactions()
+    ))
+    man_pages_list = [
+        ['commands/{0}'.format(parser_name), parser.prog.replace(' ', '-'),
+         action_helps[parser_name], 1]
+        for parser_name, parser in cli_subparsers.choices.items()
+    ]
+
+    logger.info('Write man_pages JSON file: %s',
+                os.path.relpath(man_pages_json_file))
+    with open(man_pages_json_file, 'w+') as f:
+        json.dump(man_pages_list, f, indent=2)
+
+
+def generate_command_rst_docs(cli_subparser):
+    """Write command document into source/commands/ directory
+
+    Man pages and HTML pages will be generated by sphinx from this directory.
+
+    Each document is formatted in reStructuredText.
+    """
+    for command_name, parser in cli_subparser.choices.items():
+        arguments_text = []
+        options_text = []
+        for action in parser._actions:
+            if action.option_strings:
+                # Optional arguments
+                options_text.append(command_option_template.format(
+                    name_metavar=' '.join(action.option_strings),
+                    description=format_literal(action.help)))
+            else:
+                # Positional arguments
+                arguments_text.append(command_option_template.format(
+                    name_metavar=action.dest,
+                    description=action.help,
+                ))
+
+        # We need 4 space indent for each line in usage in order to be rendered
+        # in code block.
+        # Meanwhile, the text "usage: " is not useful for rendering HTML and
+        # man pages. So, remove it. And don't forget that besides adding
+        # indent, we also have to remove 7 space characters from the second
+        # line and rest.
+        buf = six.moves.StringIO(parser.format_usage().strip())
+        first_n_chars = len('usage: ')
+        try:
+            usage = ''.join([
+                '    {0}'.format(line[first_n_chars:]) for line in buf
+            ])
+        finally:
+            buf.close()
+
+        if arguments_text:
+            arguments = command_arguments_template.format(
+                arguments=os.linesep.join(arguments_text))
+        else:
+            arguments = ''
+
+        command_rst_content = command_rst_template.format(
+            name=command_name,
+            name_section_marker='=' * len(command_name),
+            usage=usage,
+            description=parser.description or '',
+            arguments=arguments,
+            options=os.linesep.join(options_text))
+
+        filename = join(commands_docs_dir, '{0}.rst'.format(command_name))
+        logger.info('Write document: %s', os.path.relpath(filename))
+        with open(filename, 'w+') as rst:
+            rst.write(command_rst_content)
+
+
+def build_cli_parser():
+    parser = argparse.ArgumentParser(
+        description='Generate commands RST documents in docs/source/commands'
+                    ' for rendering HTML and man pages.')
+    parser.add_argument(
+        '--app-name',
+        required=False,
+        metavar='NAME',
+        dest='app_name',
+        default='sample-rpkg',
+        help='Application name of the downstream client tool built on top of'
+             ' rpkg. By default, for rpkg itself, sample-rpkg is used.')
+    parser.add_argument(
+        '-v', '--verbose',
+        required=False,
+        default=False,
+        dest='verbose',
+        action='store_true',
+        help='Verbose output.')
+    return parser
+
+
+if __name__ == '__main__':
+    import pyrpkg.cli
+
+    cli_parser = build_cli_parser()
+    args = cli_parser.parse_args()
+    if args.verbose:
+        logger.level = logging.DEBUG
+
+    client = pyrpkg.cli.cliClient(name=args.app_name, config=None)
+    logger.info('Generate man_pages list from registered commands that will be'
+                ' read in conf.py')
+    generate_man_pages_list_file(client.subparsers)
+    logger.info('Generate commands RST documents from registered commands into'
+                ' source/commands/.')
+    generate_command_rst_docs(client.subparsers)
diff --git a/doc/rpkg_man_page.py b/doc/rpkg_man_page.py
deleted file mode 100644
index 004dd59..0000000
--- a/doc/rpkg_man_page.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Print a man page from the help texts.
-#
-# Copyright (C) 2011 Red Hat Inc.
-# Author(s): Jesse Keating <jkeating@redhat.com>
-#
-# This program is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation; either version 2 of the License, or (at your
-# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
-# the full text of the license.
-
-
-import os
-import sys
-
-
-if __name__ == '__main__':
-    module_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
-    sys.path.insert(0, module_path)
-
-    import pyrpkg.man_gen
-    import pyrpkg.cli
-    client = pyrpkg.cli.cliClient(name='rpkg', config=None)
-    pyrpkg.man_gen.generate(client.parser,
-                            client.subparsers,
-                            identity='rpkg',
-                            sourceurl='https://pagure.io/rpkg/')
diff --git a/doc/source/commands.rst b/doc/source/commands.rst
new file mode 100644
index 0000000..e89a050
--- /dev/null
+++ b/doc/source/commands.rst
@@ -0,0 +1,10 @@
+.. _commands:
+
+Commands
+========
+
+.. toctree::
+   :maxdepth: 1
+   :glob:
+
+   commands/*
diff --git a/doc/source/conf.py b/doc/source/conf.py
index de50338..f291425 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -19,6 +19,7 @@
 import os
 import sys
 sys.path.insert(0, os.path.abspath('../..'))
+sys.path.insert(0, os.path.abspath('.'))
 
 # -- General configuration ------------------------------------------------
 
@@ -147,10 +148,17 @@ latex_documents = [
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, 'rpkg', u'rpkg Documentation',
-     [author], 1)
-]
+#
+# Man pages are generated from registered commands in each register_* method.
+# Script generate_man_pages.py must run before `make man'.
+
+if os.path.exists('man_pages.json'):
+    import json
+    with open('man_pages.json', 'r') as f:
+        man_pages = json.load(f)
+        [man_info.insert(3, [author]) for man_info in man_pages]
+else:
+    man_pages = []
 
 
 # -- Options for Texinfo output -------------------------------------------
diff --git a/doc/source/index.rst b/doc/source/index.rst
index bf46bb0..f57c4e3 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -11,6 +11,7 @@ Welcome to rpkg's documentation!
 
    intro
    configuration
+   commands
    api
    migrations
    release_notes

From a04b5f8dc864f5a7b315cd5c50709eef631bb4cd Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 4/15] Simplify doc Makefile


For rpkg, we only need to make html and man, and each of them requires
specific actions. This patch refines Makefile generated by
sphinx-quickstart for rpkg documentation maintenance specifically.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/Makefile b/doc/Makefile
index 8663617..ef8442f 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -2,19 +2,31 @@
 #
 
 # You can set these variables from the command line.
-SPHINXOPTS    = -E
 SPHINXBUILD   = sphinx-build
 SPHINXPROJ    = rpkg
 SOURCEDIR     = source
 BUILDDIR      = build
 
-# Put it first so that "make" without argument is like "make help".
-help:
-	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 
-.PHONY: help Makefile
+all: clean-command-docs generate-man-pages-list html man
+.PHONY: all
 
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
-	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+clean-command-docs:
+	@rm -f "$(SOURCEDIR)/commands/*.rst"
+.PHONY: clean-command-docs
+
+clean-man-pages:
+	@rm -rf "$(BUILDDIR)/man/"
+.PHONY: clean-man-pages
+
+generate-man-pages-list:
+	@python generate_man_pages.py
+.PHONY: generate-man-pages-list
+
+man: clean-man-pages
+	@$(SPHINXBUILD) -b man "$(SOURCEDIR)" "$(BUILDDIR)/man/"
+.PHONY: man
+
+html:
+	@$(SPHINXBUILD) -E -b html "$(SOURCEDIR)" "$(BUILDDIR)/html/"
+.PHONY: html

From 0151c31b0d2acf6cf1b116bedfa4cee5a7e23809 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 5/15] Generate HTML document and manpage for sample rpkg


Original script is refactored to generate HTML document and manpage for
sample executable rpkg. The final script is implemented to be general
and reusable as much as possible.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/.gitignore b/.gitignore
index 9e26969..7dad08e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,5 +20,6 @@ htmlcov/
 .env/
 
 doc/build/
+doc/source/cli.rst
 doc/source/commands/
-doc/source/man_pages.json
\ No newline at end of file
+doc/source/man_pages.json
diff --git a/doc/generate_man_pages.py b/doc/generate_man_pages.py
index 1952da5..06b883e 100644
--- a/doc/generate_man_pages.py
+++ b/doc/generate_man_pages.py
@@ -25,43 +25,16 @@ logger = logging.getLogger('Generator')
 
 join = os.path.join
 
-project_root = os.path.abspath(join(os.path.dirname(__file__), '..'))
-sys.path.insert(0, project_root)
+PROJECT_ROOT = os.path.abspath(join(os.path.dirname(__file__), '..'))
+sys.path.insert(0, PROJECT_ROOT)
 
-doc_dir = join(project_root, 'doc')
-commands_docs_dir = join(doc_dir, 'source', 'commands')
-man_pages_json_file = join(doc_dir, 'source', 'man_pages.json')
+DOC_DIR = join(PROJECT_ROOT, 'doc')
+DOC_SOURCE_DIR = join(DOC_DIR, 'source')
+COMMANDS_DOCS_DIR = join(DOC_SOURCE_DIR, 'commands')
+MAN_PAGES_JSON_FILE = join(DOC_SOURCE_DIR, 'man_pages.json')
 
-if not os.path.exists(commands_docs_dir):
-    os.mkdir(commands_docs_dir)
-
-command_rst_template = '''\
-.. _{name}:
-
-{name}
-{name_section_marker}
-
-.. program:: {name}
-
-Synopsis
---------
-
-::
-
-{usage}
-
-Description
------------
-
-{description}
-
-{arguments}
-
-Options
--------
-
-{options}
-'''
+if not os.path.exists(COMMANDS_DOCS_DIR):
+    os.mkdir(COMMANDS_DOCS_DIR)
 
 command_option_template = '''\
 .. option:: {name_metavar}
@@ -77,6 +50,29 @@ Arguments
 '''
 
 
+class TemplateNotFound(Exception):
+    """Custom document template file is not found"""
+
+
+def read_template(name, template_dir=None):
+    """Find and read template content
+
+    :param str name: the template name. If a template file name is
+        `cli.rst.tmpl`, `cli.rst` should be passed to this argument.
+    :param str template_dir: a directory from where to find custom template
+        file. If omitted, template file will be found from templates directory
+        which is located at same place with this script.
+    :return: content of the template file.
+    :raises TemplateNotFound: if template file is not found.
+    """
+    search_dir = template_dir or join(DOC_SOURCE_DIR, templates)
+    filename = join(search_dir, '{0}.tmpl'.format(name))
+    if not os.path.exists(filename):
+        raise TemplateNotFound()
+    with open(filename, 'r') as f:
+        return f.read()
+
+
 def format_literal(s):
     """Format possible inline literal text
 
@@ -87,7 +83,7 @@ def format_literal(s):
     return re.sub(regex, r'``\1``', s)
 
 
-def generate_man_pages_list_file(cli_subparsers):
+def generate_man_pages_list_file(parser, cli_subparsers, app_manpage_subject):
     """Generate registered commands list for generating man pages
 
     The generated content will be written into a JSON file. Each command will
@@ -110,18 +106,33 @@ def generate_man_pages_list_file(cli_subparsers):
         for action in cli_subparsers._get_subactions()
     ))
     man_pages_list = [
-        ['commands/{0}'.format(parser_name), parser.prog.replace(' ', '-'),
-         action_helps[parser_name], 1]
-        for parser_name, parser in cli_subparsers.choices.items()
+        [
+            'commands/{0}'.format(parser_name),
+            command_parser.prog.replace(' ', '-'),
+            action_helps[parser_name],
+            1
+        ]
+        for parser_name, command_parser in cli_subparsers.choices.items()
+    ]
+
+    # Add application manpage manually. For example, in a downstream client
+    # tool built on top of rpkg, this is for running `man fedpkg`.
+    man_pages_list += [
+        [
+            'commands/{0}'.format(parser.prog),
+            parser.prog,
+            app_manpage_subject,
+            1
+        ]
     ]
 
     logger.info('Write man_pages JSON file: %s',
-                os.path.relpath(man_pages_json_file))
-    with open(man_pages_json_file, 'w+') as f:
+                os.path.relpath(MAN_PAGES_JSON_FILE))
+    with open(MAN_PAGES_JSON_FILE, 'w+') as f:
         json.dump(man_pages_list, f, indent=2)
 
 
-def generate_command_rst_docs(cli_subparser):
+def generate_command_rst_docs(cli_subparser, template_dir=None):
     """Write command document into source/commands/ directory
 
     Man pages and HTML pages will be generated by sphinx from this directory.
@@ -165,7 +176,8 @@ def generate_command_rst_docs(cli_subparser):
         else:
             arguments = ''
 
-        command_rst_content = command_rst_template.format(
+        tmpl = read_template('command.rst', template_dir=template_dir)
+        command_rst_content = tmpl.format(
             name=command_name,
             name_section_marker='=' * len(command_name),
             usage=usage,
@@ -173,13 +185,88 @@ def generate_command_rst_docs(cli_subparser):
             arguments=arguments,
             options=os.linesep.join(options_text))
 
-        filename = join(commands_docs_dir, '{0}.rst'.format(command_name))
+        filename = join(COMMANDS_DOCS_DIR, '{0}.rst'.format(command_name))
         logger.info('Write document: %s', os.path.relpath(filename))
         with open(filename, 'w+') as rst:
             rst.write(command_rst_content)
 
 
+def generate_app_synopsis_global_options(parser, template_dir=None):
+    """
+    Generate text of synopsis and global options that will be filled into
+    cli.rst and [app name].rst inside commands directory.
+    """
+    def _format_option(formatter, action):
+        name_metavar = formatter._format_action_invocation(action)
+        description = ' '.join(formatter._expand_help(action).split('\n'))
+        return command_option_template.format(
+            name_metavar=name_metavar, description=description)
+
+    formatter = parser._get_formatter()
+    options = [
+        _format_option(formatter, action)
+        for action in parser._get_optional_actions()
+    ]
+
+    tmpl = read_template('cli_synopsis_global_options.rst',
+                         template_dir=template_dir)
+    return tmpl.format(name=parser.prog, options='\n'.join(options))
+
+
+def generate_cli_rst(parser, cli_subparsers, template_dir=None):
+    """Generate cli.rst for generating HTML document"""
+    commands_list = [
+        '   commands/{0}'.format(parser_name)
+        for parser_name, _ in cli_subparsers.choices.items()
+    ]
+    synopsis_global_options = generate_app_synopsis_global_options(
+        parser, template_dir=template_dir)
+
+    tmpl = read_template('cli.rst', template_dir=template_dir)
+    with open(join(DOC_SOURCE_DIR, 'cli.rst'), 'w+') as f:
+        f.write(tmpl.format(
+            synopsis_global_options=synopsis_global_options,
+            commands_list='\n'.join(commands_list)))
+
+
+def generate_app_rst_doc(parser, cli_subparsers, template_dir=None):
+    """
+    Generate app RST document inside commands directory for generating manpage
+
+    commands/[app name].rst is not added to a toctree. There will be a warning
+    for this. Just ignore that warning.
+
+    :param parser: the parser object of application.
+    :type parser: `argparse.ArgumentParser`
+    """
+    commands = [
+        '**{0}-{1}**\n  {2}\n'.format(parser.prog, action.dest, action.help)
+        for action in cli_subparsers._get_subactions()
+    ]
+
+    tmpl = read_template('app.rst', template_dir=template_dir)
+    synopsis_global_options = generate_app_synopsis_global_options(
+        parser, template_dir=template_dir)
+    rst_file = join(COMMANDS_DOCS_DIR, '{0}.rst'.format(parser.prog))
+    with open(rst_file, 'w+') as rst:
+        rst.write(tmpl.format(
+            name=parser.prog,
+            name_section_marker='=' * len(parser.prog),
+            synopsis_global_options=synopsis_global_options,
+            commands='\n'.join(commands)
+        ))
+
+
 def build_cli_parser():
+    def validate_template_dir(value):
+        if not os.path.exists(value):
+            raise argparse.ArgumentTypeError(
+                '{0} does not exists.'.format(value))
+        if not os.path.isdir(value):
+            raise argparse.ArgumentTypeError(
+                '{0} is not a directory.'.format(value))
+        return value
+
     parser = argparse.ArgumentParser(
         description='Generate commands RST documents in docs/source/commands'
                     ' for rendering HTML and man pages.')
@@ -198,6 +285,21 @@ def build_cli_parser():
         dest='verbose',
         action='store_true',
         help='Verbose output.')
+    parser.add_argument(
+        '--template-dir',
+        required=False,
+        metavar='DIR',
+        type=validate_template_dir,
+        default=join(DOC_DIR, 'templates'),
+        help='From where to find custom document templates.')
+    parser.add_argument(
+        '--app-manpage-subject',
+        required=False,
+        metavar='SUBJECT',
+        dest='app_manpage_subject',
+        default='A utility to build RPM, container and module',
+        help='The subject shown in sample rpkg executable manpage.'
+    )
     return parser
 
 
@@ -210,9 +312,20 @@ if __name__ == '__main__':
         logger.level = logging.DEBUG
 
     client = pyrpkg.cli.cliClient(name=args.app_name, config=None)
+
+    logger.info('Generate CLI RST for generating HTML document')
+    generate_cli_rst(
+        client.parser, client.subparsers, template_dir=args.template_dir)
+    logger.info('Generate app RST inside commands/ for generating manpage')
+    generate_app_rst_doc(
+        client.parser, client.subparsers, template_dir=args.template_dir)
+
     logger.info('Generate man_pages list from registered commands that will be'
                 ' read in conf.py')
-    generate_man_pages_list_file(client.subparsers)
+    generate_man_pages_list_file(
+        client.parser, client.subparsers, args.app_manpage_subject)
+
     logger.info('Generate commands RST documents from registered commands into'
                 ' source/commands/.')
-    generate_command_rst_docs(client.subparsers)
+    generate_command_rst_docs(
+        client.subparsers, template_dir=args.template_dir)
diff --git a/doc/source/commands.rst b/doc/source/commands.rst
deleted file mode 100644
index e89a050..0000000
--- a/doc/source/commands.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-.. _commands:
-
-Commands
-========
-
-.. toctree::
-   :maxdepth: 1
-   :glob:
-
-   commands/*
diff --git a/doc/source/index.rst b/doc/source/index.rst
index f57c4e3..ce26967 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -11,7 +11,7 @@ Welcome to rpkg's documentation!
 
    intro
    configuration
-   commands
+   cli
    api
    migrations
    release_notes
diff --git a/doc/templates/app.rst.tmpl b/doc/templates/app.rst.tmpl
new file mode 100644
index 0000000..980ff1e
--- /dev/null
+++ b/doc/templates/app.rst.tmpl
@@ -0,0 +1,24 @@
+.. _{name}:
+
+{name}
+{name_section_marker}
+
+.. program:: {name}
+
+{synopsis_global_options}
+
+Commands
+--------
+
+{commands}
+
+Reporting Bugs
+--------------
+
+Login and create a issue from https://pagure.io/rpkg/issues/.
+
+Notes
+-----
+
+* Source code: https://pagure.io/rpkg
+* Documentation: https://docs.pagure.org/rpkg/
diff --git a/doc/templates/cli.rst.tmpl b/doc/templates/cli.rst.tmpl
new file mode 100644
index 0000000..33389b8
--- /dev/null
+++ b/doc/templates/cli.rst.tmpl
@@ -0,0 +1,19 @@
+.. _cli:
+
+CLI
+===
+
+rpkg is a library to build command line utility to build RPM, container and
+module in local machine or remote build system. There is no executable to be
+shipped from rpkg project. This page shows what command line interface and
+commands will be available in any packaging tool that is built on top of rpkg.
+
+{synopsis_global_options}
+
+Commands
+--------
+
+.. toctree::
+   :maxdepth: 1
+
+{commands_list}
diff --git a/doc/templates/cli_synopsis_global_options.rst.tmpl b/doc/templates/cli_synopsis_global_options.rst.tmpl
new file mode 100644
index 0000000..a6dbaa9
--- /dev/null
+++ b/doc/templates/cli_synopsis_global_options.rst.tmpl
@@ -0,0 +1,19 @@
+Synopsis
+--------
+
+::
+
+    {name} [ global_options ] command [ command_options ] [ command_arguments ]
+    {name} help
+    {name} command --help
+
+Description
+-----------
+
+{name} is a library to build command line appliaction to build RPM, container
+and module locally or in remote build system.
+
+Global Options
+--------------
+
+{options}
diff --git a/doc/templates/command.rst.tmpl b/doc/templates/command.rst.tmpl
new file mode 100644
index 0000000..096ef11
--- /dev/null
+++ b/doc/templates/command.rst.tmpl
@@ -0,0 +1,25 @@
+.. _{name}:
+
+{name}
+{name_section_marker}
+
+.. program:: {name}
+
+Synopsis
+--------
+
+::
+
+{usage}
+
+Description
+-----------
+
+{description}
+
+{arguments}
+
+Options
+-------
+
+{options}

From ee52dd628c30ca76c57e193beb0d16d1cecfbf57 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 6/15] Rename generate_man_pages.py


New name is generate_commands_docs.py which reflects what it
actually does. The generated reStructuredText documents are not only for
building HTML documents, but also for generating manpages for the main
CLI and each subcommands.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/Makefile b/doc/Makefile
index ef8442f..bac3659 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -20,7 +20,7 @@ clean-man-pages:
 .PHONY: clean-man-pages
 
 generate-man-pages-list:
-	@python generate_man_pages.py
+	@python generate_commands_docs.py
 .PHONY: generate-man-pages-list
 
 man: clean-man-pages
diff --git a/doc/generate_commands_docs.py b/doc/generate_commands_docs.py
new file mode 100644
index 0000000..06b883e
--- /dev/null
+++ b/doc/generate_commands_docs.py
@@ -0,0 +1,331 @@
+# -*- coding: utf-8 -*-
+# generate_man_pages.py - script to generate documents for commands
+#
+# Copyright (C) 2017 Red Hat Inc.
+# Author(s): Chenxiong Qi <cqi@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
+# the full text of the license.
+
+from __future__ import print_function
+
+import argparse
+import json
+import logging
+import os
+import re
+import six
+import sys
+
+logging.basicConfig(level=logging.CRITICAL)
+logger = logging.getLogger('Generator')
+
+join = os.path.join
+
+PROJECT_ROOT = os.path.abspath(join(os.path.dirname(__file__), '..'))
+sys.path.insert(0, PROJECT_ROOT)
+
+DOC_DIR = join(PROJECT_ROOT, 'doc')
+DOC_SOURCE_DIR = join(DOC_DIR, 'source')
+COMMANDS_DOCS_DIR = join(DOC_SOURCE_DIR, 'commands')
+MAN_PAGES_JSON_FILE = join(DOC_SOURCE_DIR, 'man_pages.json')
+
+if not os.path.exists(COMMANDS_DOCS_DIR):
+    os.mkdir(COMMANDS_DOCS_DIR)
+
+command_option_template = '''\
+.. option:: {name_metavar}
+
+   {description}
+'''
+
+command_arguments_template = '''\
+Arguments
+---------
+
+{arguments}
+'''
+
+
+class TemplateNotFound(Exception):
+    """Custom document template file is not found"""
+
+
+def read_template(name, template_dir=None):
+    """Find and read template content
+
+    :param str name: the template name. If a template file name is
+        `cli.rst.tmpl`, `cli.rst` should be passed to this argument.
+    :param str template_dir: a directory from where to find custom template
+        file. If omitted, template file will be found from templates directory
+        which is located at same place with this script.
+    :return: content of the template file.
+    :raises TemplateNotFound: if template file is not found.
+    """
+    search_dir = template_dir or join(DOC_SOURCE_DIR, templates)
+    filename = join(search_dir, '{0}.tmpl'.format(name))
+    if not os.path.exists(filename):
+        raise TemplateNotFound()
+    with open(filename, 'r') as f:
+        return f.read()
+
+
+def format_literal(s):
+    """Format possible inline literal text
+
+    For example, if string contains text --some-option, it will be formatted
+    to ``--some-option``.
+    """
+    regex = '(--[a-z]+(-[a-z]+)+)'
+    return re.sub(regex, r'``\1``', s)
+
+
+def generate_man_pages_list_file(parser, cli_subparsers, app_manpage_subject):
+    """Generate registered commands list for generating man pages
+
+    The generated content will be written into a JSON file. Each command will
+    be mapped to man page information format that sphinx requires, which is
+
+    (source start file, name, description, authors, manual section)
+
+    But, we don't write the forth column authors, that will be handled
+    (inserted) by the piece of code inside `conf.py`.
+
+    So, the final JSON file will contain data dumped from this structure
+
+    [
+        ('commands/{command name}', '{command name}, 'description', 1),
+        ...
+    ]
+    """
+    action_helps = dict((
+        (action.dest, action.help)
+        for action in cli_subparsers._get_subactions()
+    ))
+    man_pages_list = [
+        [
+            'commands/{0}'.format(parser_name),
+            command_parser.prog.replace(' ', '-'),
+            action_helps[parser_name],
+            1
+        ]
+        for parser_name, command_parser in cli_subparsers.choices.items()
+    ]
+
+    # Add application manpage manually. For example, in a downstream client
+    # tool built on top of rpkg, this is for running `man fedpkg`.
+    man_pages_list += [
+        [
+            'commands/{0}'.format(parser.prog),
+            parser.prog,
+            app_manpage_subject,
+            1
+        ]
+    ]
+
+    logger.info('Write man_pages JSON file: %s',
+                os.path.relpath(MAN_PAGES_JSON_FILE))
+    with open(MAN_PAGES_JSON_FILE, 'w+') as f:
+        json.dump(man_pages_list, f, indent=2)
+
+
+def generate_command_rst_docs(cli_subparser, template_dir=None):
+    """Write command document into source/commands/ directory
+
+    Man pages and HTML pages will be generated by sphinx from this directory.
+
+    Each document is formatted in reStructuredText.
+    """
+    for command_name, parser in cli_subparser.choices.items():
+        arguments_text = []
+        options_text = []
+        for action in parser._actions:
+            if action.option_strings:
+                # Optional arguments
+                options_text.append(command_option_template.format(
+                    name_metavar=' '.join(action.option_strings),
+                    description=format_literal(action.help)))
+            else:
+                # Positional arguments
+                arguments_text.append(command_option_template.format(
+                    name_metavar=action.dest,
+                    description=action.help,
+                ))
+
+        # We need 4 space indent for each line in usage in order to be rendered
+        # in code block.
+        # Meanwhile, the text "usage: " is not useful for rendering HTML and
+        # man pages. So, remove it. And don't forget that besides adding
+        # indent, we also have to remove 7 space characters from the second
+        # line and rest.
+        buf = six.moves.StringIO(parser.format_usage().strip())
+        first_n_chars = len('usage: ')
+        try:
+            usage = ''.join([
+                '    {0}'.format(line[first_n_chars:]) for line in buf
+            ])
+        finally:
+            buf.close()
+
+        if arguments_text:
+            arguments = command_arguments_template.format(
+                arguments=os.linesep.join(arguments_text))
+        else:
+            arguments = ''
+
+        tmpl = read_template('command.rst', template_dir=template_dir)
+        command_rst_content = tmpl.format(
+            name=command_name,
+            name_section_marker='=' * len(command_name),
+            usage=usage,
+            description=parser.description or '',
+            arguments=arguments,
+            options=os.linesep.join(options_text))
+
+        filename = join(COMMANDS_DOCS_DIR, '{0}.rst'.format(command_name))
+        logger.info('Write document: %s', os.path.relpath(filename))
+        with open(filename, 'w+') as rst:
+            rst.write(command_rst_content)
+
+
+def generate_app_synopsis_global_options(parser, template_dir=None):
+    """
+    Generate text of synopsis and global options that will be filled into
+    cli.rst and [app name].rst inside commands directory.
+    """
+    def _format_option(formatter, action):
+        name_metavar = formatter._format_action_invocation(action)
+        description = ' '.join(formatter._expand_help(action).split('\n'))
+        return command_option_template.format(
+            name_metavar=name_metavar, description=description)
+
+    formatter = parser._get_formatter()
+    options = [
+        _format_option(formatter, action)
+        for action in parser._get_optional_actions()
+    ]
+
+    tmpl = read_template('cli_synopsis_global_options.rst',
+                         template_dir=template_dir)
+    return tmpl.format(name=parser.prog, options='\n'.join(options))
+
+
+def generate_cli_rst(parser, cli_subparsers, template_dir=None):
+    """Generate cli.rst for generating HTML document"""
+    commands_list = [
+        '   commands/{0}'.format(parser_name)
+        for parser_name, _ in cli_subparsers.choices.items()
+    ]
+    synopsis_global_options = generate_app_synopsis_global_options(
+        parser, template_dir=template_dir)
+
+    tmpl = read_template('cli.rst', template_dir=template_dir)
+    with open(join(DOC_SOURCE_DIR, 'cli.rst'), 'w+') as f:
+        f.write(tmpl.format(
+            synopsis_global_options=synopsis_global_options,
+            commands_list='\n'.join(commands_list)))
+
+
+def generate_app_rst_doc(parser, cli_subparsers, template_dir=None):
+    """
+    Generate app RST document inside commands directory for generating manpage
+
+    commands/[app name].rst is not added to a toctree. There will be a warning
+    for this. Just ignore that warning.
+
+    :param parser: the parser object of application.
+    :type parser: `argparse.ArgumentParser`
+    """
+    commands = [
+        '**{0}-{1}**\n  {2}\n'.format(parser.prog, action.dest, action.help)
+        for action in cli_subparsers._get_subactions()
+    ]
+
+    tmpl = read_template('app.rst', template_dir=template_dir)
+    synopsis_global_options = generate_app_synopsis_global_options(
+        parser, template_dir=template_dir)
+    rst_file = join(COMMANDS_DOCS_DIR, '{0}.rst'.format(parser.prog))
+    with open(rst_file, 'w+') as rst:
+        rst.write(tmpl.format(
+            name=parser.prog,
+            name_section_marker='=' * len(parser.prog),
+            synopsis_global_options=synopsis_global_options,
+            commands='\n'.join(commands)
+        ))
+
+
+def build_cli_parser():
+    def validate_template_dir(value):
+        if not os.path.exists(value):
+            raise argparse.ArgumentTypeError(
+                '{0} does not exists.'.format(value))
+        if not os.path.isdir(value):
+            raise argparse.ArgumentTypeError(
+                '{0} is not a directory.'.format(value))
+        return value
+
+    parser = argparse.ArgumentParser(
+        description='Generate commands RST documents in docs/source/commands'
+                    ' for rendering HTML and man pages.')
+    parser.add_argument(
+        '--app-name',
+        required=False,
+        metavar='NAME',
+        dest='app_name',
+        default='sample-rpkg',
+        help='Application name of the downstream client tool built on top of'
+             ' rpkg. By default, for rpkg itself, sample-rpkg is used.')
+    parser.add_argument(
+        '-v', '--verbose',
+        required=False,
+        default=False,
+        dest='verbose',
+        action='store_true',
+        help='Verbose output.')
+    parser.add_argument(
+        '--template-dir',
+        required=False,
+        metavar='DIR',
+        type=validate_template_dir,
+        default=join(DOC_DIR, 'templates'),
+        help='From where to find custom document templates.')
+    parser.add_argument(
+        '--app-manpage-subject',
+        required=False,
+        metavar='SUBJECT',
+        dest='app_manpage_subject',
+        default='A utility to build RPM, container and module',
+        help='The subject shown in sample rpkg executable manpage.'
+    )
+    return parser
+
+
+if __name__ == '__main__':
+    import pyrpkg.cli
+
+    cli_parser = build_cli_parser()
+    args = cli_parser.parse_args()
+    if args.verbose:
+        logger.level = logging.DEBUG
+
+    client = pyrpkg.cli.cliClient(name=args.app_name, config=None)
+
+    logger.info('Generate CLI RST for generating HTML document')
+    generate_cli_rst(
+        client.parser, client.subparsers, template_dir=args.template_dir)
+    logger.info('Generate app RST inside commands/ for generating manpage')
+    generate_app_rst_doc(
+        client.parser, client.subparsers, template_dir=args.template_dir)
+
+    logger.info('Generate man_pages list from registered commands that will be'
+                ' read in conf.py')
+    generate_man_pages_list_file(
+        client.parser, client.subparsers, args.app_manpage_subject)
+
+    logger.info('Generate commands RST documents from registered commands into'
+                ' source/commands/.')
+    generate_command_rst_docs(
+        client.subparsers, template_dir=args.template_dir)
diff --git a/doc/generate_man_pages.py b/doc/generate_man_pages.py
deleted file mode 100644
index 06b883e..0000000
--- a/doc/generate_man_pages.py
+++ /dev/null
@@ -1,331 +0,0 @@
-# -*- coding: utf-8 -*-
-# generate_man_pages.py - script to generate documents for commands
-#
-# Copyright (C) 2017 Red Hat Inc.
-# Author(s): Chenxiong Qi <cqi@redhat.com>
-#
-# This program is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation; either version 2 of the License, or (at your
-# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
-# the full text of the license.
-
-from __future__ import print_function
-
-import argparse
-import json
-import logging
-import os
-import re
-import six
-import sys
-
-logging.basicConfig(level=logging.CRITICAL)
-logger = logging.getLogger('Generator')
-
-join = os.path.join
-
-PROJECT_ROOT = os.path.abspath(join(os.path.dirname(__file__), '..'))
-sys.path.insert(0, PROJECT_ROOT)
-
-DOC_DIR = join(PROJECT_ROOT, 'doc')
-DOC_SOURCE_DIR = join(DOC_DIR, 'source')
-COMMANDS_DOCS_DIR = join(DOC_SOURCE_DIR, 'commands')
-MAN_PAGES_JSON_FILE = join(DOC_SOURCE_DIR, 'man_pages.json')
-
-if not os.path.exists(COMMANDS_DOCS_DIR):
-    os.mkdir(COMMANDS_DOCS_DIR)
-
-command_option_template = '''\
-.. option:: {name_metavar}
-
-   {description}
-'''
-
-command_arguments_template = '''\
-Arguments
----------
-
-{arguments}
-'''
-
-
-class TemplateNotFound(Exception):
-    """Custom document template file is not found"""
-
-
-def read_template(name, template_dir=None):
-    """Find and read template content
-
-    :param str name: the template name. If a template file name is
-        `cli.rst.tmpl`, `cli.rst` should be passed to this argument.
-    :param str template_dir: a directory from where to find custom template
-        file. If omitted, template file will be found from templates directory
-        which is located at same place with this script.
-    :return: content of the template file.
-    :raises TemplateNotFound: if template file is not found.
-    """
-    search_dir = template_dir or join(DOC_SOURCE_DIR, templates)
-    filename = join(search_dir, '{0}.tmpl'.format(name))
-    if not os.path.exists(filename):
-        raise TemplateNotFound()
-    with open(filename, 'r') as f:
-        return f.read()
-
-
-def format_literal(s):
-    """Format possible inline literal text
-
-    For example, if string contains text --some-option, it will be formatted
-    to ``--some-option``.
-    """
-    regex = '(--[a-z]+(-[a-z]+)+)'
-    return re.sub(regex, r'``\1``', s)
-
-
-def generate_man_pages_list_file(parser, cli_subparsers, app_manpage_subject):
-    """Generate registered commands list for generating man pages
-
-    The generated content will be written into a JSON file. Each command will
-    be mapped to man page information format that sphinx requires, which is
-
-    (source start file, name, description, authors, manual section)
-
-    But, we don't write the forth column authors, that will be handled
-    (inserted) by the piece of code inside `conf.py`.
-
-    So, the final JSON file will contain data dumped from this structure
-
-    [
-        ('commands/{command name}', '{command name}, 'description', 1),
-        ...
-    ]
-    """
-    action_helps = dict((
-        (action.dest, action.help)
-        for action in cli_subparsers._get_subactions()
-    ))
-    man_pages_list = [
-        [
-            'commands/{0}'.format(parser_name),
-            command_parser.prog.replace(' ', '-'),
-            action_helps[parser_name],
-            1
-        ]
-        for parser_name, command_parser in cli_subparsers.choices.items()
-    ]
-
-    # Add application manpage manually. For example, in a downstream client
-    # tool built on top of rpkg, this is for running `man fedpkg`.
-    man_pages_list += [
-        [
-            'commands/{0}'.format(parser.prog),
-            parser.prog,
-            app_manpage_subject,
-            1
-        ]
-    ]
-
-    logger.info('Write man_pages JSON file: %s',
-                os.path.relpath(MAN_PAGES_JSON_FILE))
-    with open(MAN_PAGES_JSON_FILE, 'w+') as f:
-        json.dump(man_pages_list, f, indent=2)
-
-
-def generate_command_rst_docs(cli_subparser, template_dir=None):
-    """Write command document into source/commands/ directory
-
-    Man pages and HTML pages will be generated by sphinx from this directory.
-
-    Each document is formatted in reStructuredText.
-    """
-    for command_name, parser in cli_subparser.choices.items():
-        arguments_text = []
-        options_text = []
-        for action in parser._actions:
-            if action.option_strings:
-                # Optional arguments
-                options_text.append(command_option_template.format(
-                    name_metavar=' '.join(action.option_strings),
-                    description=format_literal(action.help)))
-            else:
-                # Positional arguments
-                arguments_text.append(command_option_template.format(
-                    name_metavar=action.dest,
-                    description=action.help,
-                ))
-
-        # We need 4 space indent for each line in usage in order to be rendered
-        # in code block.
-        # Meanwhile, the text "usage: " is not useful for rendering HTML and
-        # man pages. So, remove it. And don't forget that besides adding
-        # indent, we also have to remove 7 space characters from the second
-        # line and rest.
-        buf = six.moves.StringIO(parser.format_usage().strip())
-        first_n_chars = len('usage: ')
-        try:
-            usage = ''.join([
-                '    {0}'.format(line[first_n_chars:]) for line in buf
-            ])
-        finally:
-            buf.close()
-
-        if arguments_text:
-            arguments = command_arguments_template.format(
-                arguments=os.linesep.join(arguments_text))
-        else:
-            arguments = ''
-
-        tmpl = read_template('command.rst', template_dir=template_dir)
-        command_rst_content = tmpl.format(
-            name=command_name,
-            name_section_marker='=' * len(command_name),
-            usage=usage,
-            description=parser.description or '',
-            arguments=arguments,
-            options=os.linesep.join(options_text))
-
-        filename = join(COMMANDS_DOCS_DIR, '{0}.rst'.format(command_name))
-        logger.info('Write document: %s', os.path.relpath(filename))
-        with open(filename, 'w+') as rst:
-            rst.write(command_rst_content)
-
-
-def generate_app_synopsis_global_options(parser, template_dir=None):
-    """
-    Generate text of synopsis and global options that will be filled into
-    cli.rst and [app name].rst inside commands directory.
-    """
-    def _format_option(formatter, action):
-        name_metavar = formatter._format_action_invocation(action)
-        description = ' '.join(formatter._expand_help(action).split('\n'))
-        return command_option_template.format(
-            name_metavar=name_metavar, description=description)
-
-    formatter = parser._get_formatter()
-    options = [
-        _format_option(formatter, action)
-        for action in parser._get_optional_actions()
-    ]
-
-    tmpl = read_template('cli_synopsis_global_options.rst',
-                         template_dir=template_dir)
-    return tmpl.format(name=parser.prog, options='\n'.join(options))
-
-
-def generate_cli_rst(parser, cli_subparsers, template_dir=None):
-    """Generate cli.rst for generating HTML document"""
-    commands_list = [
-        '   commands/{0}'.format(parser_name)
-        for parser_name, _ in cli_subparsers.choices.items()
-    ]
-    synopsis_global_options = generate_app_synopsis_global_options(
-        parser, template_dir=template_dir)
-
-    tmpl = read_template('cli.rst', template_dir=template_dir)
-    with open(join(DOC_SOURCE_DIR, 'cli.rst'), 'w+') as f:
-        f.write(tmpl.format(
-            synopsis_global_options=synopsis_global_options,
-            commands_list='\n'.join(commands_list)))
-
-
-def generate_app_rst_doc(parser, cli_subparsers, template_dir=None):
-    """
-    Generate app RST document inside commands directory for generating manpage
-
-    commands/[app name].rst is not added to a toctree. There will be a warning
-    for this. Just ignore that warning.
-
-    :param parser: the parser object of application.
-    :type parser: `argparse.ArgumentParser`
-    """
-    commands = [
-        '**{0}-{1}**\n  {2}\n'.format(parser.prog, action.dest, action.help)
-        for action in cli_subparsers._get_subactions()
-    ]
-
-    tmpl = read_template('app.rst', template_dir=template_dir)
-    synopsis_global_options = generate_app_synopsis_global_options(
-        parser, template_dir=template_dir)
-    rst_file = join(COMMANDS_DOCS_DIR, '{0}.rst'.format(parser.prog))
-    with open(rst_file, 'w+') as rst:
-        rst.write(tmpl.format(
-            name=parser.prog,
-            name_section_marker='=' * len(parser.prog),
-            synopsis_global_options=synopsis_global_options,
-            commands='\n'.join(commands)
-        ))
-
-
-def build_cli_parser():
-    def validate_template_dir(value):
-        if not os.path.exists(value):
-            raise argparse.ArgumentTypeError(
-                '{0} does not exists.'.format(value))
-        if not os.path.isdir(value):
-            raise argparse.ArgumentTypeError(
-                '{0} is not a directory.'.format(value))
-        return value
-
-    parser = argparse.ArgumentParser(
-        description='Generate commands RST documents in docs/source/commands'
-                    ' for rendering HTML and man pages.')
-    parser.add_argument(
-        '--app-name',
-        required=False,
-        metavar='NAME',
-        dest='app_name',
-        default='sample-rpkg',
-        help='Application name of the downstream client tool built on top of'
-             ' rpkg. By default, for rpkg itself, sample-rpkg is used.')
-    parser.add_argument(
-        '-v', '--verbose',
-        required=False,
-        default=False,
-        dest='verbose',
-        action='store_true',
-        help='Verbose output.')
-    parser.add_argument(
-        '--template-dir',
-        required=False,
-        metavar='DIR',
-        type=validate_template_dir,
-        default=join(DOC_DIR, 'templates'),
-        help='From where to find custom document templates.')
-    parser.add_argument(
-        '--app-manpage-subject',
-        required=False,
-        metavar='SUBJECT',
-        dest='app_manpage_subject',
-        default='A utility to build RPM, container and module',
-        help='The subject shown in sample rpkg executable manpage.'
-    )
-    return parser
-
-
-if __name__ == '__main__':
-    import pyrpkg.cli
-
-    cli_parser = build_cli_parser()
-    args = cli_parser.parse_args()
-    if args.verbose:
-        logger.level = logging.DEBUG
-
-    client = pyrpkg.cli.cliClient(name=args.app_name, config=None)
-
-    logger.info('Generate CLI RST for generating HTML document')
-    generate_cli_rst(
-        client.parser, client.subparsers, template_dir=args.template_dir)
-    logger.info('Generate app RST inside commands/ for generating manpage')
-    generate_app_rst_doc(
-        client.parser, client.subparsers, template_dir=args.template_dir)
-
-    logger.info('Generate man_pages list from registered commands that will be'
-                ' read in conf.py')
-    generate_man_pages_list_file(
-        client.parser, client.subparsers, args.app_manpage_subject)
-
-    logger.info('Generate commands RST documents from registered commands into'
-                ' source/commands/.')
-    generate_command_rst_docs(
-        client.subparsers, template_dir=args.template_dir)

From e58247be6c80d98ada0f40791e71f3deb0229e1e Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 7/15] Fix MANIFEST.in to list files for building doc


Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/MANIFEST.in b/MANIFEST.in
index 74b1109..9043293 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -7,11 +7,17 @@ include bin/rpkg
 recursive-include tests *
 recursive-include etc *
 
-recursive-include doc *.rst *.py
-prune doc/source/commands/
-
 include requirements/README.rst
 include requirements/*.txt
 
 global-exclude *.pyc
 global-exclude __pycache__
+
+include doc/Makefile
+recursive-include doc *.rst *.py *.tmpl
+
+# RST files under commands directory and other listed files are generated by
+# script when run make each time. They should not be included source
+# distribution package.
+prune doc/source/commands/
+exclude doc/source/cli.rst doc/source/man_pages.json

From a6eed01009f0ed44f5286a5caac1d5992da4864b Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 8/15] Fix rebase error: add module_build_watch back


Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py
index a952dc5..3d85e3e 100644
--- a/pyrpkg/cli.py
+++ b/pyrpkg/cli.py
@@ -1861,6 +1861,10 @@ see API KEY section of copr-cli(1) man page.
                 api_url.rstrip('/'), api_version)
         return self._module_api_url
 
+    def module_build_watch(self):
+        """Watch an MBS build from the command-line"""
+        self.module_watch_build([self.args.build_id])
+
     def module_overview(self):
         """Show the overview of the latest builds in the MBS"""
         self.module_validate_config()

From fda9158b2f2c7b3f45e828de3390190decb5a623 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 9/15] Ensure to clean files for generating HTML documents


Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/Makefile b/doc/Makefile
index bac3659..7e5c430 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -11,9 +11,10 @@ BUILDDIR      = build
 all: clean-command-docs generate-man-pages-list html man
 .PHONY: all
 
-clean-command-docs:
+clean-files:
 	@rm -f "$(SOURCEDIR)/commands/*.rst"
-.PHONY: clean-command-docs
+	@rm $(SOURCEDIR)/cli.rst
+.PHONY: clean-files
 
 clean-man-pages:
 	@rm -rf "$(BUILDDIR)/man/"
@@ -27,6 +28,6 @@ man: clean-man-pages
 	@$(SPHINXBUILD) -b man "$(SOURCEDIR)" "$(BUILDDIR)/man/"
 .PHONY: man
 
-html:
+html: clean-files generate-man-pages-list
 	@$(SPHINXBUILD) -E -b html "$(SOURCEDIR)" "$(BUILDDIR)/html/"
 .PHONY: html

From 321f3c623e9c947bae305fbdfbf0d1de1819f4ae Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 10/15] Exclude subcommand which does not have help


gitcred was added and does not have help by adding `add_help=False`.
Such subcommands will be excluded in the generated documentation.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/generate_commands_docs.py b/doc/generate_commands_docs.py
index 06b883e..8e10e8e 100644
--- a/doc/generate_commands_docs.py
+++ b/doc/generate_commands_docs.py
@@ -113,6 +113,7 @@ def generate_man_pages_list_file(parser, cli_subparsers, app_manpage_subject):
             1
         ]
         for parser_name, command_parser in cli_subparsers.choices.items()
+        if parser_name in action_helps
     ]
 
     # Add application manpage manually. For example, in a downstream client
@@ -139,7 +140,8 @@ def generate_command_rst_docs(cli_subparser, template_dir=None):
 
     Each document is formatted in reStructuredText.
     """
-    for command_name, parser in cli_subparser.choices.items():
+    for command_name, parser in [item for item in cli_subparser.choices.items()
+                                 if item[1].add_help]:
         arguments_text = []
         options_text = []
         for action in parser._actions:
@@ -217,7 +219,8 @@ def generate_cli_rst(parser, cli_subparsers, template_dir=None):
     """Generate cli.rst for generating HTML document"""
     commands_list = [
         '   commands/{0}'.format(parser_name)
-        for parser_name, _ in cli_subparsers.choices.items()
+        for parser_name, parser in cli_subparsers.choices.items()
+        if parser.add_help
     ]
     synopsis_global_options = generate_app_synopsis_global_options(
         parser, template_dir=template_dir)

From a77f9d71abc85cbf04b1a6bab5e9a55aea3e54d3 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 11/15] Do not generate document for sample rpkg app


There is already generated cli.rst. Therefor, no need to generate a
sample-rpkg.rst inside source/commands/.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/generate_commands_docs.py b/doc/generate_commands_docs.py
index 8e10e8e..f9e611d 100644
--- a/doc/generate_commands_docs.py
+++ b/doc/generate_commands_docs.py
@@ -232,34 +232,6 @@ def generate_cli_rst(parser, cli_subparsers, template_dir=None):
             commands_list='\n'.join(commands_list)))
 
 
-def generate_app_rst_doc(parser, cli_subparsers, template_dir=None):
-    """
-    Generate app RST document inside commands directory for generating manpage
-
-    commands/[app name].rst is not added to a toctree. There will be a warning
-    for this. Just ignore that warning.
-
-    :param parser: the parser object of application.
-    :type parser: `argparse.ArgumentParser`
-    """
-    commands = [
-        '**{0}-{1}**\n  {2}\n'.format(parser.prog, action.dest, action.help)
-        for action in cli_subparsers._get_subactions()
-    ]
-
-    tmpl = read_template('app.rst', template_dir=template_dir)
-    synopsis_global_options = generate_app_synopsis_global_options(
-        parser, template_dir=template_dir)
-    rst_file = join(COMMANDS_DOCS_DIR, '{0}.rst'.format(parser.prog))
-    with open(rst_file, 'w+') as rst:
-        rst.write(tmpl.format(
-            name=parser.prog,
-            name_section_marker='=' * len(parser.prog),
-            synopsis_global_options=synopsis_global_options,
-            commands='\n'.join(commands)
-        ))
-
-
 def build_cli_parser():
     def validate_template_dir(value):
         if not os.path.exists(value):
@@ -319,9 +291,6 @@ if __name__ == '__main__':
     logger.info('Generate CLI RST for generating HTML document')
     generate_cli_rst(
         client.parser, client.subparsers, template_dir=args.template_dir)
-    logger.info('Generate app RST inside commands/ for generating manpage')
-    generate_app_rst_doc(
-        client.parser, client.subparsers, template_dir=args.template_dir)
 
     logger.info('Generate man_pages list from registered commands that will be'
                 ' read in conf.py')

From 3256611afa732e59c04957f7a27040659fd7e1e8 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:52:12 +0000
Subject: [PATCH 12/15] Remove warning of nonexisting source/_static/ during doc build


Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/source/_static/.placeholder b/doc/source/_static/.placeholder
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/doc/source/_static/.placeholder

From ef27c223cbbf4c93eff0db82ddba790b8ee56f8b Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 04:56:38 +0000
Subject: [PATCH 13/15] Run document generator script in Py3 explictly


Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/Makefile b/doc/Makefile
index 7e5c430..3530802 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -21,7 +21,7 @@ clean-man-pages:
 .PHONY: clean-man-pages
 
 generate-man-pages-list:
-	@python generate_commands_docs.py
+	@python3 generate_commands_docs.py
 .PHONY: generate-man-pages-list
 
 man: clean-man-pages

From b255fbc83620a3493521d19dd550274600b1b092 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 06:38:52 +0000
Subject: [PATCH 14/15] Fix mistakes during rebase


Some old code was added back, which is fixed in this commit.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py
index 3d85e3e..90df637 100644
--- a/pyrpkg/cli.py
+++ b/pyrpkg/cli.py
@@ -1734,7 +1734,6 @@ see API KEY section of copr-cli(1) man page.
     def module_build_cancel(self):
         """Cancel an MBS build"""
         api_url = self.module_api_url
-        self.module_validate_config()
         build_id = self.args.build_id
         auth_method, oidc_id_provider, oidc_client_id, oidc_client_secret, \
             oidc_scopes = self.module_get_auth_config()
@@ -1750,9 +1749,6 @@ see API KEY section of copr-cli(1) man page.
     def module_build_info(self):
         """Show information about an MBS build"""
         self.cmd.module_build_info(self.module_api_url, self.args.build_id)
-        self.module_validate_config()
-        api_url = self.config.get(self.config_section, 'api_url')
-        self.cmd.module_build_info(api_url, self.args.build_id)
 
     def module_build_local(self):
         """Build a module locally using mbs-manager"""
@@ -1867,8 +1863,6 @@ see API KEY section of copr-cli(1) man page.
 
     def module_overview(self):
         """Show the overview of the latest builds in the MBS"""
-        self.module_validate_config()
-        api_url = self.config.get(self.config_section, 'api_url')
         self.cmd.module_overview(
             self.module_api_url, self.args.limit,
             finished=(not self.args.unfinished))

From f790cb1902143ac67b66d619e55adca603c392e8 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jun 12 2018 07:17:37 +0000
Subject: [PATCH 15/15] Minor fixes to doc build


* Removing nonexisting cli.rst in a fresh clone does not break.
* Fix undefined variable name and shadow names in generator script.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/doc/Makefile b/doc/Makefile
index 3530802..6b8eef4 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -13,7 +13,7 @@ all: clean-command-docs generate-man-pages-list html man
 
 clean-files:
 	@rm -f "$(SOURCEDIR)/commands/*.rst"
-	@rm $(SOURCEDIR)/cli.rst
+	@rm -f $(SOURCEDIR)/cli.rst
 .PHONY: clean-files
 
 clean-man-pages:
diff --git a/doc/generate_commands_docs.py b/doc/generate_commands_docs.py
index f9e611d..7e0d152 100644
--- a/doc/generate_commands_docs.py
+++ b/doc/generate_commands_docs.py
@@ -65,7 +65,7 @@ def read_template(name, template_dir=None):
     :return: content of the template file.
     :raises TemplateNotFound: if template file is not found.
     """
-    search_dir = template_dir or join(DOC_SOURCE_DIR, templates)
+    search_dir = template_dir or join(DOC_SOURCE_DIR, 'templates')
     filename = join(search_dir, '{0}.tmpl'.format(name))
     if not os.path.exists(filename):
         raise TemplateNotFound()
@@ -219,8 +219,8 @@ def generate_cli_rst(parser, cli_subparsers, template_dir=None):
     """Generate cli.rst for generating HTML document"""
     commands_list = [
         '   commands/{0}'.format(parser_name)
-        for parser_name, parser in cli_subparsers.choices.items()
-        if parser.add_help
+        for parser_name, cmd_parser in cli_subparsers.choices.items()
+        if cmd_parser.add_help
     ]
     synopsis_global_options = generate_app_synopsis_global_options(
         parser, template_dir=template_dir)

