You read the description of this class method, but still don’t understand what is happening. If only you could quickly read the source code…
Python’s power lies not just in its simplicity and efficiency but also in its vast community and rich Documentation. But what if you could make that documentation even more interactive and informative? Today, I’ll walk you through enhancing your Sphinx-generated Python documentation by linking it directly to the corresponding source code on GitHub.
Step 1: Documenting with Sphinx
When we write proper docstrings in our Python code, we lay the groundwork for generating comprehensive API documentation. Tools like Sphinx’s autodoc
and automodule
are great for extracting these docstrings from our modules, classes, and functions. But, they typically fall short in providing direct links to the source code.
If you need to get started with Sphinx, check these tutorials:
Building a Documentation Project with Sphinx – Intro to Documentation with Sphinx and…
Step 2: Setting up Sphinx linkcode
To add this functionality, first, we need to modify our Sphinx configuration. This involves adding sphinx.ext.linkcode
to our list of extensions in the conf.py
file of our documentation source:
# docs/conf.py
extensions = [
...,
"sphinx.ext.linkcode",
]
...
Step 3: Basic Linkcode Implementation
Our next step is to define the linkcode_resolve
function. This function is responsible for determining the URL that the documentation should point to:
# docs/conf.py
...
def linkcode_resolve(domain, info):
if domain != "py":
return None
if not info["module"]:
return None
filename = "src/" + info["module"].replace(".", "/")
github_repo = "https://github.com/username/my-package"
return f"{github_repo}/blob/main/{filename}.py"
Here, we’re simply pointing to the file in the GitHub repository but not yet to the specific line of code.
Step 4: Finding the Line Number
Getting the Module Object
First, you’re obtaining the module object where the target class, method, attribute, or function (henceforth simply referred to as ‘object’) is defined. In Python, every loaded module is stored in a dictionary called sys.modules
. You’re accessing this dictionary to retrieve the module object based on its name:
module = sys.modules.get(info["module"])
Iterating Over the Fully Qualified Name
Next, you iterate over the fully qualified name of the object. A fully qualified name includes all the hierarchical levels through which the object can be accessed, like module.class.method
. This iteration helps you to dig into the module structure and reach the exact object you need:
obj = module
for part in info["fullname"].split("."):
obj = getattr(obj, part, None)
Using the Inspect Module to Find the Line Number
Finally, you use the inspect
module to find the line number in the source code where the definition of this object begins:
line = inspect.getsourcelines(obj)[1]
Making one function to retrieve the line number
To make a function that works for all cases, we need to add some extra checks:
def get_object_line_number(info):
"""Return object line number from module."""
try:
module = sys.modules.get(info["module"])
if module is None:
return None
# walk through the nested module structure
obj = module
for part in info["fullname"].split("."):
obj = getattr(obj, part, None)
if obj is None:
return None
return inspect.getsourcelines(obj)[1]
except (TypeError, OSError):
return None
In summary, this step involves locating the module where the target object is defined, traversing the module’s structure to find the exact object (be it a class, method, attribute, or function), and then using the inspect
module to find the line number where this object is defined in the source code.
Step 6: Finalizing Linkcode Resolution
We integrate the line number retrieval into the linkcode_resolve
function:
def linkcode_resolve(domain, info):
...
line = get_object_line_number(info)
if line is None:
return None
return f"{github_repo}/blob/{github_branch}/{filename}.py#L{line}"
This approach allows your documentation to provide a direct link to the specific line in the source code, enhancing the clarity and usefulness of the documentation.
Extra: Branch Adaptation for ReadTheDocs
For those using ReadTheDocs, you can adapt this function to reference different branches (like main
or develop
). So how to build two different documentations pointing to each of those branches?
When ReadTheDocs builds the documentation, it uses it’s own environment with a variable called READTHEDOCS_VERSION
, which typically is ‘stable’ or ‘latest’. I have added another build called ‘develop’ that points to the branch of the same name on my git repository.
We can add the following to our linkcode_resolve
function:
def linkcode_resolve(domain, info):
...
rtd_version = os.getenv("READTHEDOCS_VERSION", "latest")
github_branch = "develop" if rtd_version == "develop" else "main"
return f"{github_repo}/blob/{github_branch}/{filename}.py#L{line}"
Note that if building locally, or more generally in an environment where READTHEDOCS_VERSION
is not defined, the default branch linked will be main
.
Feel free to check the GitHub repository where this is implemented, and the documentation.
Conclusion
With this setup, your Sphinx documentation will now have direct links to the source code, greatly enhancing its utility and user experience. This small addition can significantly improve the usability and effectiveness of your documentation, making it an invaluable tool for both new learners and experienced developers alike.
Remember, the journey of coding is as much about sharing knowledge and solutions as it is about solving problems. So, if you found this tutorial helpful, I encourage you to spread the word and help others in their coding adventures.
Happy coding, and until next time!
Discover more of my work and exclusive content on my Gumroad profile or personal website.
Disclaimer: I do not hold any affiliation with Sphinx, I only find it a great Open Source tool.