Extracting pydoc Comments With autodoc
00:00 In the previous lesson, I showed you RST markup and how to use it in a Sphinx document. In this lesson, I’ll be talking about the autodoc extension that allows you to incorporate the comments in your code into your documentation.
00:14 Sphinx has a mechanism for writing extensions and in fact, it ships with some. The one I’m going to be showing you is called autodoc and it adds directives for extracting pydoc comments from your code and including them in your documentation.
00:28
Autodoc comes with the Sphinx package, so you don’t have to install anything additional. You do have to configure it though. Open up your conf.py
file and add sphinx.ext.autodoc
as a string to the extensions list to enable it.
00:45
The autodoc extension directive that I use most frequently is automodule
. This takes a module file name as an argument and causes autodoc
to parse that file for pydoc comments.
00:57 The names of classes and functions with pydoc comments get pulled into your documentation. There are a bunch of different options available for the directive.
01:08 You can explicitly list those members of the module you want to include, or you can leave it blank and have them all included. You can specify whether to include private members in your docs, those being things that start with single or double underscores.
01:23
The special-members
option will include dunder methods from a class. If present, you can optionally list those dunder methods you’re interested in explicitly as well.
01:33
By default, autodoc only looks for things that have docstring comments. If you add the undoc-members
option, it will also include members without pydoc comments.
01:46
exclude-members
allows you to specify anything you wish to exclude and member-order
allows you to change what order they appear in.
01:55
I typically have a method to my code ordering madness when I write my code, so I like to use the bysource
argument to member-order
so that the docs show the same order as my code.
02:08 Rather than include a whole module, you can explicitly include a code thing. There are directives for classes, functions, decorators, data items, methods, and attributes.
02:21
To demonstrate all of this, I have added some source to my Serenity Project. The serenity/
directory now has two directories inside of it. The docs/
one I created before and a new one for my source.
02:34
I’m going to add a new RST file to the docs that uses the automodule
directive to pull in the contents of my source code. The source is split into two files, one file containing a class and the other a function.
02:50
This is what the whole project tree looks like. In the project directory, I added README.rst
and in the docs directory, I’ve added serenity.rst
.
03:00
The source directory has the Serenity code in it. This module has a ship
and actions
and __init__
Python file. Inside __init__
I’ve put a variable containing the version number for my release.
03:14
I’m going to update the conf.py
file to use this value so I don’t have to update the release number in two places.
03:24 Inside of your pydoc comments, you can use special notations to cross-reference to other pieces of code. All of these are variations on references for classes.
03:34
Relative referencing can be a bit finicky and as you might move your classes into new files, I find the best thing to do is always just use the second one, the fully named module. Prefacing that with a tilde (~
) will mean the resulting link will only show the class name if you prefer it to be shorter.
03:53 There are similar notations for functions, attributes and modules themselves. In HTML all of these will end up as links to the place in your document where that specific thing has been incorporated.
04:08
In addition to cross-referencing code, there are some notations you can use to help describe part of a function or method inside of your pydoc. If you use the param
notation, Sphinx will build a little table of your arguments to the function.
04:22
The returns
notation is to document what your function or method responds with.
04:30
If you want to bring a bit of style to your documentation, you can choose a different theme. Themes are available through PyPI and are pip
installed.
04:39
Once you’ve got it installed, you need to tell the output that it is using the theme you want to do that. This is done in the conf.py
file. I’ll be using the Read the Docs theme in this demo.
04:51
You can get that by pip
installing sphinx-rtd-theme
. Note that the package name uses hyphens, but the module uses underscores. Remember that for when I show you the config.
05:03 Okay, you’re all set to incorporate pydoc comments into your documentation. Let’s go take a look.
05:11
This is the conf.py
file in the docs directory. I’ve messed with it a bit. First off, I’ve changed the copyright variable from a simple string to a bit of code.
05:22
I grab the datetime
class, use it to calculate the current year, and then if the year is later than 2024, (that’s the year of this recording) I make the copyright value have a range in it.
05:34
Otherwise, it’s just the year and author like before. All of this is possible because conf.py
is, well, a .py
file. This file gets run every time you build, so you can make your logic as convoluted as you like.
05:49 If you can do it in Python, you can do it in your config.
05:54
Recall that I mentioned that I added a dunder version variable to the __init__
file in the Serenity module. These four lines add the source/
directory to the path that Python uses to search for modules, then imports the serenity
module from them.
06:09
The __version__
variable from the module then gets copied into the release value. This means I only have to bump my version number in one place: my code. And all my docs can stay in sync. Scrolling down.
06:24 The next change was to add the extension configuration. As I mentioned before, that’s just a matter of adding a string with the full module name of the extension to this list.
06:34
As autodoc comes with Sphinx, this is all you need to use the automodule
directive. Finally, I’m changing the look and feel of the HTML build by setting the html_theme
value to the Read the Docs theme as promised.
06:50
Okay, with the config changed, let’s go look at my index.rst
file.
06:59
Earlier I mentioned this little trick. Often the contents of your doc homepage is going to be the same as the README for your GitHub repo. As GitHub supports the RST format, you can write a README.rst
file.
07:13
Then use the include
directive to suck that into your index.rst
file. This way you don’t have to write the content twice. Although I did change how release
gets populated, this line doesn’t change.
07:27
It’ll still pull it in, referencing the newly dynamically loaded variable from conf.py
I’m going to put my pydoc stuff into a file called serenity.rst
, so like with movie.rst
, I’ve added it to my table of contents.
07:45 Let’s go look at the README file.
07:49 And here it is. This time I remembered to use the title style heading the way I should have before. Nothing fancy in this file, just a few sentences, but the magic is that it can be both my GitHub landing page for the project as well as the document homepage.
08:11 This is an RST file, like any other. You can write as much or as little as you like here. If all your info is in your pydoc comments, this doc might be short, like in this example.
08:22
If you need to write something more or include images or whatnot, you can RST to your heart’s content. The first automodule
directive here references the ship
module.
08:33
Note that the name of the module is fully qualified. It needs both serenity
and ship
to find the right one. The members
option means all the contents of this file will be parsed.
08:44
The special-members
option means dunder methods will also be included in the output.
08:50
My second automodule
directive is similar, but this one is for the actions
file and that’s it. With that in place, the docs are ready to go.
08:59 Let’s look at the code that this file is going to be parsing.
09:05
This is my __init__
file. As promised, I have declared a __version__
variable to hold my release number. This is what gets read in by conf.py
and it can also be used by my module itself.
09:21
This is ship.py
. Up at the top here, I have my first pydoc comment, the pydoc for the class itself. It’s actually good practice to put this here rather than in, say, the __init__
as __init__
doesn’t always get included.
09:38
Speaking of __init__
, this part of the pydoc is using the param
notation to describe the passengers
argument for the __init__
method.
09:48
And similarly here inside of the crazy_ivan
method, I’m using the returns
notation to indicate that crazy_ivan
returns the new direction.
10:04
The class
marker here will result in a link to the Firefly
class documentation.
10:10
This second reference uses the angle bracket (<>
) style to change how the name is displayed. And this is a reference to the pilot
attribute inside the class. Attributes don’t get linked, but they do get styled appropriately.
10:25 Note that although I’m using these references in the pydoc itself, you can use them anywhere in your documentation. If one of your RST files mentions a class or function, you can use the same notation there to deep link into the docs for that class or function.
10:41
I’ll skip the boring make html
part. Let’s go see the result.
10:48 The new look and feel here is because of the Read the Docs theme being applied. The content next to my pointer is the README file that was included. Notice how the nav on the left and the table of contents now have the links to the Serenity module.
11:06
Let me click that and here is the autodoc
content. A couple things to note: because I used the special-members
option to the automodule
directive but didn’t explicitly list which dunder methods I’ve got all of them, __init__
and __weakref__
.
11:25
If you only wanted __init__
you could specify that in the option to the automodule
directive. Note how the parameters and return markers are being presented, showing the description.
11:38
This is even clearer on the change_pilot
function where you have multiple arguments: a nice little listing is created.
11:46 The cross-reference here isn’t fantastic as it points to the same page, but it is a fully qualified link with a hash actor. Clicking it does send me to the top of the class, seeing if that’s all on the same page.
11:59 There’s not much to see by doing that though.
12:04
One little caveat, autodoc
actually imports your code. That means your code has to be importable. In the case of frameworks like Django, things can go wrong if you haven’t got the environment set up correctly. As conf.py
is a script, you can do whatever setup you need though.
12:23
This little snippet works for Django, it adds the parent directory to the sys.path
, imports django
and runs Django’s required setup.
12:30
Depending on your framework and where you’ve put your code, this snippet would need to change. In fact, if I recall correctly, the code I pulled this out of didn’t use an src
directory, so that path statement would likely need to change if Serenity was a Django thing. You might have to fiddle a little bit based on what framework you’re using, but you get the general idea.
12:51
Whatever setup you need can be done in conf.py
.
12:55 and there you go, you’ve got some docs. Next up I’ll show you how to host them at Read the Docs.
Christopher Trudeau RP Team on Oct. 16, 2024
Hi David,
Yes, imp
was part of the standard library and isn’t there anymore so pip
won’t help.
The new way of doing dynamic imports is to use the importlib
library instead. The problem though is the importlib
library doesn’t have an equivalent to the load_source
call that reads Python from a code file.
What I’ve been doing lately instead is to add the src directory where the code lives to the Python load path and then load the code itself. To add your source directory to the Python load path:
import sys
from pathlib import Path
SRC_DIR = Path(__file__).parent / '../src'
SRC_DIR = SRC_DIR.resolve()
sys.path.insert(0, str(SRC_DIR))
And once you’ve done that you can get at your code. Which you can then use to get at a version variable. For example:
import serenity
release = serenity.__version__
Give that a try and see if it helps.
Become a Member to join the conversation.
david on Oct. 16, 2024
Hello, unfortunately this does not work on my computer: I get the following error:
I tried
pip install imp
, but it does not work. docs.python.org/3.11/library/imp.html saysCould you please help? Thanks!