Reputation: 24034
I'm writing a lightweight class whose attributes are intended to be publicly accessible, and only sometimes overridden in specific instantiations. There's no provision in the Python language for creating docstrings for class attributes, or any sort of attributes, for that matter. What is the expected and supported way, should there be one, to document these attributes? Currently I'm doing this sort of thing:
class Albatross(object):
"""A bird with a flight speed exceeding that of an unladen swallow.
Attributes:
"""
flight_speed = 691
__doc__ += """
flight_speed (691)
The maximum speed that such a bird can attain.
"""
nesting_grounds = "Raymond Luxury-Yacht"
__doc__ += """
nesting_grounds ("Raymond Luxury-Yacht")
The locale where these birds congregate to reproduce.
"""
def __init__(self, **keyargs):
"""Initialize the Albatross from the keyword arguments."""
self.__dict__.update(keyargs)
This will result in the class's docstring containing the initial standard docstring section, as well as the lines added for each attribute via augmented assignment to __doc__
.
Although this style doesn't seem to be expressly forbidden in the docstring style guidelines, it's also not mentioned as an option. The advantage here is that it provides a way to document attributes alongside their definitions, while still creating a presentable class docstring, and avoiding having to write comments that reiterate the information from the docstring. I'm still kind of annoyed that I have to actually write the attributes twice; I'm considering using the string representations of the values in the docstring to at least avoid duplication of the default values.
Is this a heinous breach of the ad hoc community conventions? Is it okay? Is there a better way? For example, it's possible to create a dictionary containing values and docstrings for the attributes and then add the contents to the class __dict__
and docstring towards the end of the class declaration; this would alleviate the need to type the attribute names and values twice. edit: this last idea is, I think, not actually possible, at least not without dynamically building the entire class from data, which seems like a really bad idea unless there's some other reason to do that.
I'm pretty new to python and still working out the details of coding style, so unrelated critiques are also welcome.
Upvotes: 191
Views: 148807
Reputation: 59
Here's an answer that abuses ast
and inspect
. It does nothing to the original class implementation besides changing the docstring.
import ast
import inspect
from io import StringIO
def ast_find_classdef(tree):
for e in ast.walk(tree):
if isinstance(e, ast.ClassDef):
return e
def attribute_docs(cls):
"""Enable attribute documentations for (data)classes. Use this function as a decorator.
```
@attribute_docs
@dataclass
class TargetCoder:
"Target coder for object detection"
"Number of detection classes"
num_classes: int
"Normalized minimum bounding box size"
min_size: flaot
```
"""
# == find the class defining syntax tree ==
src = inspect.getsource(cls)
tree = ast.parse(src)
tree = ast_find_classdef(tree)
# == gather attribute doc strings ==
# * We skip the first expr, because it is either a class docstring or something else
# * The idea is that docstring appears on top of the attribute.
# * Therefore, we search for any string node, mark that as a docstring.
# * If a class attribute define node appears after the docstring, we store the docstring
# along with the class attribute's information
attribute_docs = {}
last_doc: Optional[str] = None
for expr in tree.body[1:]:
# When encouter an Expr, check if the expr a string
if isinstance(expr, ast.Expr):
# The value is a ast.Value node
# therefore another access to value is needed
value = expr.value.value
if isinstance(value, str):
last_doc = value.strip()
# if the last known doc string is not none
# and this next node is an annotation, that's a docstring
if isinstance(expr, ast.AnnAssign) and last_doc is not None:
# expr.target is a ast.Name
name = ast.unparse(expr.target)
type_name = ast.unparse(expr.annotation)
attribute_docs[name] = (type_name, last_doc)
last_doc = None
# == Append to the class documentation ==
# * if there is no attribute docstring, leave it be
if len(attribute_docs) > 0:
old_docs = cls.__doc__
append_docs = build_attibute_docstrings(attribute_docs)
cls.__doc__ = f"""{old_docs}\n\n{append_docs}"""
return cls
def build_attibute_docstrings(docs):
# Create pretty formatting for the attribute docs
with StringIO() as io:
io.write("Attributes:\n")
for var_name, (type_name, docstring) in docs.items():
# == Multiline vs inline doc format ==
# * if the doc is inline, simply use the `x (type): docstring`
# * if the doc is multiline, create a new paragraph
if "\n" in docstring:
lines = docstring.split("\n")
lines = ["\t\t" + line.strip() for line in lines]
docstring = "\n".join(lines)
line = f"\t{var_name} ({type_name}):\n{docstring}\n"
else:
line = f"\t{var_name} ({type_name}): {docstring}\n"
# Add the docstring line
io.write(line)
io.seek(0)
docstring = io.read()
return docstring
@attribute_docs
@dataclass
class DBNetAlignCoder:
"DBNet target coder for aligned case, i.e. detection targets are axis-aligned"
"Number of detection classes"
num_classes: int
"Input image width"
image_width: int
"Input image height"
image_height: int
"""Shrink rate of bounding boxes, the shrink distance will be computed using
[A * (1 - r^2) / L], where A is the bounding box area, L is the bounding box
perimeter, and r is the shrink ratio
"""
shrink_ratio: float
"Minimum probability to be considered a positive detection"
det_threshold: float
"""
Whether to use a simple threshold map drawing method. If true, the threshold
map values will be 1, instead of the distance from shrink/expand boxes to the
actual boxes as described in the DBNet paper.
"""
simple_threshold: bool = False
The output of help()
:
class DBNetAlignCoder(builtins.object)
| DBNetAlignCoder(num_classes: int, image_width: int, image_height: int, shrink_ratio: float, det_threshold: float, simple_threshold: bool = False) -> None
|
| DBNet target coder for aligned case, i.e. detection targets are axis-aligned
|
| Attributes:
| num_classes (int): Number of detection classes
| image_width (int): Input image width
| image_height (int): Input image height
| shrink_ratio (float):
| Shrink rate of bounding boxes, the shrink distance will be computed using
| [A * (1 - r^2) / L], where A is the bounding box area, L is the bounding box
| perimeter, and r is the shrink ratio
| det_threshold (float): Minimum probability to be considered a positive detection
| simple_threshold (bool):
| Whether to use a simple threshold map drawing method. If true, the threshold
| map values will be 1, instead of the distance from shrink/expand boxes to the
| actual boxes as described in the DBNet paper.
Upvotes: 1
Reputation: 96556
The other answers are very outdated. PEP-257 describes how you can use docstrings for attributes. They come after the attribute, weirdly:
String literals occurring elsewhere in Python code may also act as documentation. They are not recognized by the Python bytecode compiler and are not accessible as runtime object attributes (i.e. not assigned to __doc__), but two types of extra docstrings may be extracted by software tools:
- String literals occurring immediately after a simple assignment at the top level of a module, class, or __init__ method are called “attribute docstrings”.
class C:
"class C doc-string"
a = 1
"attribute C.a doc-string (1)"
b = 2
"attribute C.b doc-string (2)"
It also works for type annotations like this:
class C:
"class C doc-string"
a: int
"attribute C.a doc-string (1)"
b: str
"attribute C.b doc-string (2)"
VSCode supports showing these.
Upvotes: 87
Reputation: 1258
You cite the PEP257: Docstring Conventions, in the section What is a docstring it is stated:
String literals occurring elsewhere in Python code may also act as documentation. They are not recognized by the Python bytecode compiler and are not accessible as runtime object attributes (i.e. not assigned to
__doc__
), but two types of extra docstrings may be extracted by software tools:String literals occurring immediately after a simple assignment at the top level of a module, class, or
__init__
method are called "attribute docstrings".
And this is explained in more details in the PEP 258: Attribute Docstrings section.
As explains above,
an attribute is not an object that can own a __doc__
so they won't appear in help()
or pydoc. These docstrings can only be used for generated documentation.
They are used in Sphinx with the directive autoattribute.
Sphinx can use comments on a line before an assignment or a special comment following an assignment or a docstring after the definition which will be autodocumented.
Upvotes: 54
Reputation: 23459
In short: class attributes cannot have doc strings in the way that classes and functions have.
To avoid confusion, the term property has a specific meaning in python. What you're talking about is what we call class attributes. Since they are always acted upon through their class, I find that it makes sense to document them within the class' doc string. Something like this:
class Albatross(object):
"""A bird with a flight speed exceeding that of an unladen swallow.
Attributes:
flight_speed The maximum speed that such a bird can attain.
nesting_grounds The locale where these birds congregate to reproduce.
"""
flight_speed = 691
nesting_grounds = "Throatwarbler Man Grove"
I think that's a lot easier on the eyes than the approach in your example. If I really wanted a copy of the attribute values to appear in the doc string, I would put them beside or below the description of each attribute.
Keep in mind that in Python, doc strings are actual members of the objects they document, not merely source code annotations. Since class attribute variables are not objects themselves but references to objects, they have no way of holding doc strings of their own. I guess you could make a case for doc strings on references, perhaps to describe "what should go here" instead of "what is actually here", but I find it easy enough to do that in the containing class doc string.
Upvotes: 132
Reputation: 26445
You could abuse properties to this effect. Properties contain a getter, a setter, a deleter, and a docstring. Naively, this would get very verbose:
class C:
def __init__(self):
self._x = None
@property
def x(self):
"""Docstring goes here."""
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
Then you will have a docstring belonging to C.x:
In [24]: print(C.x.__doc__)
Docstring goes here.
To do this for many attributes is cumbersome, but you could envision a helper function myprop:
def myprop(x, doc):
def getx(self):
return getattr(self, '_' + x)
def setx(self, val):
setattr(self, '_' + x, val)
def delx(self):
delattr(self, '_' + x)
return property(getx, setx, delx, doc)
class C:
a = myprop("a", "Hi, I'm A!")
b = myprop("b", "Hi, I'm B!")
In [44]: c = C()
In [46]: c.b = 42
In [47]: c.b
Out[47]: 42
In [49]: print(C.b.__doc__)
Hi, I'm B!
Then, calling Pythons interactive help
will give:
Help on class C in module __main__:
class C
| Data descriptors defined here:
|
| a
| Hi, I'm A!
|
| b
| Hi, I'm B!
which I think should be pretty much what you're after.
Edit: I realise now that we can perhaps avoid to need to pass the first argument to myprop
at all, because the internal name doesn't matter. If subsequent calls of myprop
can somehow communicate with each other, it could automatically decide upon a long and unlikely internal attribute name. I'm sure there are ways to implement this, but I'm not sure if they're worth it.
Upvotes: 21