Self-documenting Python stuff*

(* stuff is of course a technical term)

A useful side-effect of using Python as the language for your automation work is the ability to use Sphinx to generate documentation for your modules, page objects and even scripts. There are a hoops you need to setup first though.

Let’s for clarity sake say that we want to have our docs installed in a directory called ‘docs’ — makes sense. This part is pretty automated using the sphinx-quickstart

Adam-Gouchers-MacBook:Client-Selenium adam$ sphinx-quickstart
Welcome to the Sphinx 1.0.6 quickstart utility.
 
Please enter values for the following settings (just press Enter to
accept a default value, if one is given in brackets).
 
Enter the root path for documentation.
> Root path for the documentation [.]: docs
 
You have two options for placing the build directory for Sphinx output.
Either, you use a directory "_build" within the root path, or you separate
"source" and "build" directories within the root path.
> Separate source and build directories (y/N) [n]: y
 
Inside the root directory, two more directories will be created; "_templates"
for custom HTML templates and "_static" for custom stylesheets and other static
files. You can enter another prefix (such as ".") to replace the underscore.
> Name prefix for templates and static dir [_]: 
 
The project name will occur in several places in the built documentation.
> Project name: Client
> Author name(s): Adam Goucher
 
Sphinx has the notion of a "version" and a "release" for the
software. Each version can have multiple releases. For example, for
Python the version is something like 2.5 or 3.0, while the release is
something like 2.5.1 or 3.0a1.  If you don't need this dual structure,
just set both to the same value.
> Project version: .3
> Project release [.3]: 
 
The file name suffix for source files. Commonly, this is either ".txt"
or ".rst".  Only files with this suffix are considered documents.
> Source file suffix [.rst]: 
 
One document is special in that it is considered the top node of the
"contents tree", that is, it is the root of the hierarchical structure
of the documents. Normally, this is "index", but if your "index"
document is a custom template, you can also set this to another filename.
> Name of your master document (without suffix) [index]: 
 
Sphinx can also add configuration for epub output:
> Do you want to use the epub builder (y/N) [n]: 
 
Please indicate if you want to use one of the following Sphinx extensions:
> autodoc: automatically insert docstrings from modules (y/N) [n]: y
> doctest: automatically test code snippets in doctest blocks (y/N) [n]: 
> intersphinx: link between Sphinx documentation of different projects (y/N) [n]: 
> todo: write "todo" entries that can be shown or hidden on build (y/N) [n]: 
> coverage: checks for documentation coverage (y/N) [n]: 
> pngmath: include math, rendered as PNG images (y/N) [n]: 
> jsmath: include math, rendered in the browser by JSMath (y/N) [n]: 
> ifconfig: conditional inclusion of content based on config values (y/N) [n]: 
> viewcode: include links to the source code of documented Python objects (y/N) [n]: 
 
A Makefile and a Windows command file can be generated for you so that you
only have to run e.g. `make html' instead of invoking sphinx-build
directly.
> Create Makefile? (Y/n) [y]: 
> Create Windows command file? (Y/n) [y]: 
 
Finished: An initial directory structure has been created.
 
You should now populate your master file docs/source/index.rst and create other documentation
source files. Use the Makefile to build the docs, like so:
   make builder
where "builder" is one of the supported builders, e.g. html, latex or linkcheck.
 
Adam-Gouchers-MacBook:Client-Selenium adam$

There will now be docs directory which in turn has a build and source directory. Eventually we’ll care about the build directory, but for now we’re going to concentrate on the source directory.

Sphinx uses reStructuredText as its formatting language and so files that end with .rst are ones that you need to be concerned about; starting with index.rst. You could add each thing that you want documented into that file, but that’s not very organized. So we need to tune things a little by changing how it finds other bits of rst.

.. toctree::
   :maxdepth: 2
   :glob:
 
   pages*

With this change it will now load any file that starts with pages. (Eventually you will want to add modules*, pages*, providers*, etc.)

With the index setup, its time to actually create a pages.rst and in it put

Page Objects
============
 
.. automodule:: pages.login_page
  :members: LoginPage

As you might have guessed, this says that we’re going to create a page called Page Objects and it is going to be including the information from a module called login_page in the pages package. And there will be some magic controlled by the keyword automodule. The easiest way to show what that magic is is with code.

The first part is right at the beginning of the file. It is what it will be listed as in the docs. One thing that you will discover is you need to have the rows of = above and below the same length as your name. If you forget this, you get a warning when generating the docs.

"""
===============
pages.LoginPage
===============
"""

The next bunch of things; the imports, the locators, element implementations, etc. don’t need documenting and can be quietly ignored by the doc generation.

from pages.BasePage import BasePage
from pages.BaseTextElement import BaseTextElement
from SeleniumWrapper import SeleniumWrapper as wrapper
 
import ConfigWrapper
 
locators = {
    "username": "username",
    "password": "password",
    "submit_button": "submit"
}
 
class UsernameElement(BaseTextElement):
    def __init__(self):
        self.locator = locators["username"]
 
class PasswordElement(BaseTextElement):
    def __init__(self):
        self.locator = locators["password"]

Now we’re at the actual Page Object. Using a standard documentation string for the class, we describe what the page is for.

class LoginPage(BasePage):
    """
    Page Object for the login page
    """

The only real variation on comments/docstrings to be worried about is how you tell Sphinx about the properties of classes. In this case, where we map our element implementation to the Page Object class. For this you use #: on the line above the property.

    #: the salutation element at the top right
    salutation = SalutationElement()
 
    def __init__(self):
        self.se = wrapper().connection

Actions, being methods, get the same docstring treatment as the class itself.

    def do_login(self):
        """
        Do the login
        """
        self.se.click(locators["submit_button"])

Some actions will take an argument, which can also be described. Likewise, they can also return things such as other Page Objects which can be linked.

    def do_something_else(self, num):
    """
    Does something else
 
    :params num: which tab number to get
    :returns: :class:`pages.OtherPage.OtherPage`
    """
    return OtherPage.OtherPage()

And now we’re at the point where we can actually generate the docs. Change directories to the docs directory and simply do a make html — and hopefully you see something like this.

Adam-Gouchers-MacBook:docs adam$ make html
sphinx-build -b html -d build/doctrees   source build/html
Making output directory...
Running Sphinx v1.0.6
loading pickled environment... not yet created
building [html]: targets for 1 source files that are out of date
updating environment: 1 added, 0 changed, 0 removed
reading sources... [100%] index                                                                           
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
preparing documents... done
writing output... [100%] index                                                                            
writing additional files... genindex search
copying static files... done
dumping search index... done
dumping object inventory... done
build succeeded.
 
Build finished. The HTML pages are in build/html.
Adam-Gouchers-MacBook:docs adam$

Except that the first time you run it, it will whine about your PYTHONPATH; so make sure you set it appropriately for your environment and try it again.

Depending on how your code is written, that might be enough to get things generated and you can then load html/index.html in a browser and see documentation that looks very similar to Python’s own module documentation.

Then repeat for the rest of your files.

One other troubleshooting tip. Sphinx will actually try to execute the Python files it is told to document. Which means if you are trying to use singletons that are not initialized, you’ll have to wrap the offending lines in some exception handling.

def __get__(self, obj, cls=None):
    try:
        return str(wrapper().connection.get_text(self.locator))
    except AttributeError as e:
        if str(e) == "'SeleniumWrapper' object has no attribute 'connection'":
            pass
        else:
            raise e

Now, you can actually repeat the steps for all your other files.

Post a Comment

Your email is never published nor shared. Required fields are marked *