Docstring for variable
Question:
Is it posible to use docstring for plain variable? For example I have module called t
def f():
"""f"""
l = lambda x: x
"""l"""
and I do
>>> import t
>>> t.f.__doc__
'f'
but
>>> t.l.__doc__
>>>
Example is similar to PEP 258‘s (search for “this is g”).
Answers:
No, you can only do this for modules, (lambda and “normal”) functions and classes, as far as I know. Other objects, even mutable ones inherit the docstrings of their class and raise AttributeError
if you try to change that:
>>> a = {}
>>> a.__doc__ = "hello"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'dict' object attribute '__doc__' is read-only
(Your second example is valid Python, but the string """l"""
doesn’t do anything. It is generated, evaluated and discarded.)
Some python documentation scripts have notation that can be use in the module/classes docstring to document a var.
E.g. for spinx, you can use :var and :ivar. See this document (about half-way down).
Use typing.Annotated
to provide a docstring for variables.
I originally wrote an answer (see below) where I said this wasn’t possible. That was true back in 2012 but Python has moved on. Today you can provide the equivalent of a docstring for a global variable or an attribute of a class or instance. You will need to be running at least Python 3.9 for this to work:
from __future__ import annotations
from typing import Annotated
Feet = Annotated[float, "feet"]
Seconds = Annotated[float, "seconds"]
MilesPerHour = Annotated[float, "miles per hour"]
day: Seconds = 86400
legal_limit: Annotated[MilesPerHour, "UK national limit for single carriageway"] = 60
current_speed: MilesPerHour
def speed(distance: Feet, time: Seconds) -> MilesPerHour:
"""Calculate speed as distance over time"""
fps2mph = 3600 / 5280 # Feet per second to miles per hour
return distance / time * fps2mph
You can access the annotations at run time using typing.get_type_hints()
:
Python 3.9.1 (default, Jan 19 2021, 09:36:39)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import calc
>>> from typing import get_type_hints
>>> hints = get_type_hints(calc, include_extras=True)
>>> hints
{'day': typing.Annotated[float, 'seconds'], 'legal_limit': typing.Annotated[float, 'miles per hour', 'UK national limit for single carriageway'], 'current_speed': typing.Annotated[float, 'miles per hour']}
Extract information about variables using the hints for the module or class where they were declared. Notice how the annotations combine when you nest them:
>>> hints['legal_limit'].__metadata__
('miles per hour', 'UK national limit for single carriageway')
>>> hints['day']
typing.Annotated[float, 'seconds']
It even works for variables that have type annotations but have not been assigned a value. If I tried to reference calc.current_speed I would get an attribute error but I can still access its metadata:
>>> hints['current_speed'].__metadata__
('miles per hour',)
The type hints for a module only include the global variables, to drill down you need to call get_type_hints()
again on functions or classes:
>>> get_type_hints(calc.speed, include_extras=True)
{'distance': typing.Annotated[float, 'feet'], 'time': typing.Annotated[float, 'seconds'], 'return': typing.Annotated[float, 'miles per hour']}
I only know of one tool so far that can use typing.Annotated
to store documentation about a variable and that is Pydantic. It is slightly more complicated than just storing a docstring though it actually expects an instance of pydantic.Field
. Here’s an example:
from typing import Annotated
import typing_extensions
from pydantic import Field
from pydantic.main import BaseModel
from datetime import date
# TypeAlias is in typing_extensions for Python 3.9:
FirstName: typing_extensions.TypeAlias = Annotated[str, Field(
description="The subject's first name", example="Linus"
)]
class Subject(BaseModel):
# Using an annotated type defined elsewhere:
first_name: FirstName = ""
# Documenting a field inline:
last_name: Annotated[str, Field(
description="The subject's last name", example="Torvalds"
)] = ""
# Traditional method without using Annotated
# Field needs an extra argument for the default value
date_of_birth: date = Field(
...,
description="The subject's date of birth",
example="1969-12-28",
)
Using the model class:
>>> guido = Subject(first_name='Guido', last_name='van Rossum', date_of_birth=date(1956, 1, 31))
>>> print(guido)
first_name='Guido' last_name='van Rossum' date_of_birth=datetime.date(1956, 1, 31)
Pydantic models can give you a JSON schema:
>>> from pprint import pprint
>>> pprint(Subject.schema())
{'properties': {'date_of_birth': {'description': "The subject's date of birth",
'example': '1969-12-28',
'format': 'date',
'title': 'Date Of Birth',
'type': 'string'},
'first_name': {'default': '',
'description': "The subject's first name",
'example': 'Linus',
'title': 'First Name',
'type': 'string'},
'last_name': {'default': '',
'description': "The subject's last name",
'example': 'Torvalds',
'title': 'Last Name',
'type': 'string'}},
'required': ['date_of_birth'],
'title': 'Subject',
'type': 'object'}
>>>
If you use this class in a FastAPI application the OpenApi specification has example and description for all three of these taken from the relevant Field.
And here’s the original answer which was true back then but hasn’t stood the test of time:
No, it is not possible and it wouldn’t be useful if you could.
The docstring is always an attribute of an object (module, class or function), not tied to a specific variable.
That means if you could do:
t = 42
t.__doc__ = "something" # this raises AttributeError: '__doc__' is read-only
you would be setting the documentation for the integer 42 not for the variable t
. As soon as you rebind t
you lose the docstring. Immutable objects such as numbers of strings sometimes have a single object shared between different users, so in this example you would probably actually have set the docstring for all occurences of 42
throughout your program.
print(42 .__doc__) # would print "something" if the above worked!
For mutable objects it wouldn’t necessarily be harmful but would still be of limited use if you rebind the object.
If you want to document an attribute of a class then use the class’s docstring to describe it.
Epydoc allows for docstrings on variables:
While the language doesn’t directly provides for them, Epydoc supports
variable docstrings: if a variable assignment statement is immediately
followed by a bare string literal, then that assignment is treated as
a docstring for that variable.
Example:
class A:
x = 22
"""Docstring for class variable A.x"""
def __init__(self, a):
self.y = a
"""Docstring for instance variable A.y"""
Well, even though Python does not treat strings defined immediately after a global definition as a docstring for the variable, sphinx does and it is certainly not a bad practice to include them.
debug = False
'''Set to True to turn on debugging mode. This enables opening IPython on
exceptions.
'''
Here is some code that will scan a module and pull out names of global variable definitions, the value and a docstring that follows.
def GetVarDocs(fname):
'''Read the module referenced in fname (often <module>.__file__) and return a
dict with global variables, their value and the "docstring" that follows
the definition of the variable
'''
import ast,os
fname = os.path.splitext(fname)[0]+'.py' # convert .pyc to .py
with open(fname, 'r') as f:
fstr = f.read()
d = {}
key = None
for node in ast.walk(ast.parse(fstr)):
if isinstance(node,ast.Assign):
key = node.targets[0].id
d[key] = [node.value.id,'']
continue
elif isinstance(node,ast.Expr) and key:
d[key][1] = node.value.s.strip()
key = None
return d
To add to to ford’s answer about Epydoc, note that PyCharm will also use a string literal as the documentation for a variable in a class:
class Fields_Obj:
DefaultValue=None
"""Get/set the default value of the data field"""
Sphinx has a built-in syntax for documenting attributes (i.e. NOT the values as @duncan describes). Examples:
#: This is module attribute
x = 42
class MyClass:
#: This is a class attribute
y = 43
You can read more in the Sphinx docs: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autoattribute
…or in this other question: How to document a module constant in Python?
Properties can have docstrings! This covers the most common use case of documenting instance variables.
class A:
def __init__(self):
self._x = 22
@property
def x(self):
"document x"
return self._x
@x.setter
def x(self, value):
self._x = value
A.x.__doc__
A lot of answers assume you want it for offline use and points to sphinx or Epydoc.
But if you want it for runtime use the answer is that is impossible to add an attribute to another attribute. So you can’t attach a doctring to variable.
When you do:
a = True
print(a.__doc__)
You’ll be getting the docstring for the bool class.
In my application I need it for plug-ins. What I do is to use an associated variable/attribute.
Something like this:
a = True
_help_a = "help for a variable"
As this looks ugly what I’m actually using are syntactic macros (take a look a macropy module). The code looks like this:
with document:
a = True
""" help for a variable """
I explain the whole idea here
Yes, you can do that!
You can actually ‘document’ lambdas and variables in a module by attaching docstrings to them.
Not properties. Not class attributes. Variables, constants and lambdas. No Sphinx, no Epydoc. No typing.annotations
. Plain old help()
. Real docstrings. In the line of
VAR = value # (apply magicka to add docstring 'This is a variable')
help(VAR) # shows 'This is a variable'
Well, sort of …
The trick is to use decorators.
You take anything that can hold a docstring (class or function), apply decorator to it, but instead of returning modified class or wrapper class as you usually do with decorators, you can just return an object of completely different class generated on the fly with a docstring attached to it.
Something like this:
# This is ugly, yes, but it's gonna be even uglier, strap in.
@decorator_generator(lambda x: x+1)
class foo: """ docstrings """
# The decorator generator returns a decorator, which, in turn, can return
# ANYTHING AT ALL, which will be assigned to `foo`. We can use that.
The types will be very messy, but, hey, who needs them anyways.
We can also do some module class magic to reattach the docs every time a new value is assigned to module attributes, thus preserving the docs on assignment.
Consider this:
# one.py
# We import sys to use the module magicka later
import sys
def documented_named(baseclass, *init_args, **init_kwargs):
def _decorator(decorated):
class _documented_class(baseclass):
def __str__(self):
return decorated.__name__
__doc__ = decorated.__doc__
def __documented__(self):
return (documented_named, baseclass, decorated)
return _documented_class(*init_args, **init_kwargs)
return _decorator
def documented(baseclass, *init_args, **init_kwargs):
def _decorator(decorated):
class _documented_class(baseclass):
__doc__ = decorated.__doc__
def __documented__(self):
return (documented, baseclass, decorated)
return _documented_class(*init_args, **init_kwargs)
return _decorator
def documented_lambda(__lambda):
def _decorator(decorated):
def _lambda(*args):
return __lambda(*args)
_lambda.__doc__ = decorated.__doc__
return _lambda
return _decorator
class nopreserve: pass
def documented_ducktype(baseclass, *init_args, **init_kwargs):
def _decorator(decorated):
class _documented_class(baseclass):
__doc__ = decorated.__doc__
def __documented__(self):
return (documented, nopreserve, decorated)
return _documented_class(*init_args, **init_kwargs)
return _decorator
# Module magix
_thismodule = sys.modules[__name__]
class _preserve_documented(_thismodule.__class__):
def __setattr__(self, attr, value):
ex = getattr(self, attr, None)
if ex is not None and hasattr(ex, '__documented__'):
decorator, baseclass, decorated = ex.__documented__()
if baseclass is nopreserve:
baseclass = value.__class__
super().__setattr__(attr, decorator(baseclass, value)(decorated))
else:
super().__setattr__(attr,value)
_thismodule.__class__ = _preserve_documented
@documented(int, 100)
class VAR1: """ DOCUMENTATION FOR VAR1 """
@documented_named(float, 7.891011)
class VAR2: """ DOCUMENTATION FOR VAR2 """
@documented_lambda(lambda x: x+1)
class LAMBDA: """ LAMBDA DOCUMENTATION PROVIDED HERE """
@documented_ducktype(int, 100)
class VAR3:
""" DOCUMENTATION FOR VAR3
Multiline, eh?
"""
Let’s test it like this:
# two.py
import one
from one import VAR2
# inspect is here for convenience only, not needed for docs to work
import inspect
def printvar(v):
print('str: ', v)
print('repr: ', repr(v))
print('docs: ', inspect.getdoc(v))
print('')
print('Here you go, documented variables and lambda:')
for v in [one.VAR1, one.VAR2, one.LAMBDA]:
printvar(v)
# And then we set vars and check again
one.VAR1 = 12345
one.VAR2 = 789
print('nnAfter setting VAR1 and VAR2 with <module>.<varname> = foo to new values:')
for v in [one.VAR1, one.VAR2]:
printvar(v)
print('Cool, the docs are preserved.nn')
print('Note that we have also preserved the types, though this is not really necessary:')
print('VAR3:')
printvar(one.VAR3)
one.VAR3 = 'VAR3 is a string now'
print('And after assignment:')
printvar(one.VAR3)
print('VAR2 imported with "from <module> import VAR2":')
printvar(VAR2)
print('However, if we set it here we will obviously loose the doc:')
VAR2 = 999
printvar(VAR2)
print('And the types are terrible:')
print('one.VAR1: ', type(one.VAR1))
print('one.VAR2: ', type(one.VAR2))
print('one.LAMBDA:', type(one.LAMBDA))
The output:
$ python two.py
Here you go, documented variables and lambda:
str: 100
repr: 100
docs: DOCUMENTATION FOR VAR1
str: VAR2
repr: 7.891011
docs: DOCUMENTATION FOR VAR2
str: <function documented_lambda.<locals>._decorator.<locals>._lambda at 0x7fa582443790>
repr: <function documented_lambda.<locals>._decorator.<locals>._lambda at 0x7fa582443790>
docs: LAMBDA DOCUMENTATION PROVIDED HERE
After setting VAR1 and VAR2 with <module>.<varname> = foo to new values:
str: 12345
repr: 12345
docs: DOCUMENTATION FOR VAR1
str: VAR2
repr: 789.0
docs: DOCUMENTATION FOR VAR2
Cool, the docs are preserved.
Note that we have also preserved the types, though this is not really necessary:
VAR3:
str: 100
repr: 100
docs: DOCUMENTATION FOR VAR3
Multiline, eh?
And after assignment:
str: VAR3 is a string now
repr: 'VAR3 is a string now'
docs: DOCUMENTATION FOR VAR3
Multiline, eh?
VAR2 imported with "from <module> import VAR2":
str: VAR2
repr: 7.891011
docs: DOCUMENTATION FOR VAR2
However, if we set it here we will obviously loose the doc:
str: 999
repr: 999
docs: int([x]) -> integer
int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments
are given. If x is a number, return x.__int__(). For floating point
numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string,
bytes, or bytearray instance representing an integer literal in the
given base. The literal can be preceded by '+' or '-' and be surrounded
by whitespace. The base defaults to 10. Valid bases are 0 and 2-36.
Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0)
4
And the types are terrible:
one.VAR1: <class 'one.documented.<locals>._decorator.<locals>._documented_class'>
one.VAR2: <class 'one.documented_named.<locals>._decorator.<locals>._documented_class'>
one.LAMBDA: <class 'function'>
And yes, you can actually import one
and get the docs with help(one.VAR1)
this way.
And I suppose Sphinx and Epydoc can handle that pretty well if they can be forced to ignore the decorator (I did NOT test that).
Note that you can also do something similar with __metaclass__
, which should look better syntax-wise, but you’ll need Python 3.9 for that.
And I should get a time machine, this WAS totally possible since the decorators were introduced.
Is it posible to use docstring for plain variable? For example I have module called t
def f():
"""f"""
l = lambda x: x
"""l"""
and I do
>>> import t
>>> t.f.__doc__
'f'
but
>>> t.l.__doc__
>>>
Example is similar to PEP 258‘s (search for “this is g”).
No, you can only do this for modules, (lambda and “normal”) functions and classes, as far as I know. Other objects, even mutable ones inherit the docstrings of their class and raise AttributeError
if you try to change that:
>>> a = {}
>>> a.__doc__ = "hello"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'dict' object attribute '__doc__' is read-only
(Your second example is valid Python, but the string """l"""
doesn’t do anything. It is generated, evaluated and discarded.)
Some python documentation scripts have notation that can be use in the module/classes docstring to document a var.
E.g. for spinx, you can use :var and :ivar. See this document (about half-way down).
Use typing.Annotated
to provide a docstring for variables.
I originally wrote an answer (see below) where I said this wasn’t possible. That was true back in 2012 but Python has moved on. Today you can provide the equivalent of a docstring for a global variable or an attribute of a class or instance. You will need to be running at least Python 3.9 for this to work:
from __future__ import annotations
from typing import Annotated
Feet = Annotated[float, "feet"]
Seconds = Annotated[float, "seconds"]
MilesPerHour = Annotated[float, "miles per hour"]
day: Seconds = 86400
legal_limit: Annotated[MilesPerHour, "UK national limit for single carriageway"] = 60
current_speed: MilesPerHour
def speed(distance: Feet, time: Seconds) -> MilesPerHour:
"""Calculate speed as distance over time"""
fps2mph = 3600 / 5280 # Feet per second to miles per hour
return distance / time * fps2mph
You can access the annotations at run time using typing.get_type_hints()
:
Python 3.9.1 (default, Jan 19 2021, 09:36:39)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import calc
>>> from typing import get_type_hints
>>> hints = get_type_hints(calc, include_extras=True)
>>> hints
{'day': typing.Annotated[float, 'seconds'], 'legal_limit': typing.Annotated[float, 'miles per hour', 'UK national limit for single carriageway'], 'current_speed': typing.Annotated[float, 'miles per hour']}
Extract information about variables using the hints for the module or class where they were declared. Notice how the annotations combine when you nest them:
>>> hints['legal_limit'].__metadata__
('miles per hour', 'UK national limit for single carriageway')
>>> hints['day']
typing.Annotated[float, 'seconds']
It even works for variables that have type annotations but have not been assigned a value. If I tried to reference calc.current_speed I would get an attribute error but I can still access its metadata:
>>> hints['current_speed'].__metadata__
('miles per hour',)
The type hints for a module only include the global variables, to drill down you need to call get_type_hints()
again on functions or classes:
>>> get_type_hints(calc.speed, include_extras=True)
{'distance': typing.Annotated[float, 'feet'], 'time': typing.Annotated[float, 'seconds'], 'return': typing.Annotated[float, 'miles per hour']}
I only know of one tool so far that can use typing.Annotated
to store documentation about a variable and that is Pydantic. It is slightly more complicated than just storing a docstring though it actually expects an instance of pydantic.Field
. Here’s an example:
from typing import Annotated
import typing_extensions
from pydantic import Field
from pydantic.main import BaseModel
from datetime import date
# TypeAlias is in typing_extensions for Python 3.9:
FirstName: typing_extensions.TypeAlias = Annotated[str, Field(
description="The subject's first name", example="Linus"
)]
class Subject(BaseModel):
# Using an annotated type defined elsewhere:
first_name: FirstName = ""
# Documenting a field inline:
last_name: Annotated[str, Field(
description="The subject's last name", example="Torvalds"
)] = ""
# Traditional method without using Annotated
# Field needs an extra argument for the default value
date_of_birth: date = Field(
...,
description="The subject's date of birth",
example="1969-12-28",
)
Using the model class:
>>> guido = Subject(first_name='Guido', last_name='van Rossum', date_of_birth=date(1956, 1, 31))
>>> print(guido)
first_name='Guido' last_name='van Rossum' date_of_birth=datetime.date(1956, 1, 31)
Pydantic models can give you a JSON schema:
>>> from pprint import pprint
>>> pprint(Subject.schema())
{'properties': {'date_of_birth': {'description': "The subject's date of birth",
'example': '1969-12-28',
'format': 'date',
'title': 'Date Of Birth',
'type': 'string'},
'first_name': {'default': '',
'description': "The subject's first name",
'example': 'Linus',
'title': 'First Name',
'type': 'string'},
'last_name': {'default': '',
'description': "The subject's last name",
'example': 'Torvalds',
'title': 'Last Name',
'type': 'string'}},
'required': ['date_of_birth'],
'title': 'Subject',
'type': 'object'}
>>>
If you use this class in a FastAPI application the OpenApi specification has example and description for all three of these taken from the relevant Field.
And here’s the original answer which was true back then but hasn’t stood the test of time:
No, it is not possible and it wouldn’t be useful if you could.
The docstring is always an attribute of an object (module, class or function), not tied to a specific variable.
That means if you could do:
t = 42
t.__doc__ = "something" # this raises AttributeError: '__doc__' is read-only
you would be setting the documentation for the integer 42 not for the variable t
. As soon as you rebind t
you lose the docstring. Immutable objects such as numbers of strings sometimes have a single object shared between different users, so in this example you would probably actually have set the docstring for all occurences of 42
throughout your program.
print(42 .__doc__) # would print "something" if the above worked!
For mutable objects it wouldn’t necessarily be harmful but would still be of limited use if you rebind the object.
If you want to document an attribute of a class then use the class’s docstring to describe it.
Epydoc allows for docstrings on variables:
While the language doesn’t directly provides for them, Epydoc supports
variable docstrings: if a variable assignment statement is immediately
followed by a bare string literal, then that assignment is treated as
a docstring for that variable.
Example:
class A:
x = 22
"""Docstring for class variable A.x"""
def __init__(self, a):
self.y = a
"""Docstring for instance variable A.y"""
Well, even though Python does not treat strings defined immediately after a global definition as a docstring for the variable, sphinx does and it is certainly not a bad practice to include them.
debug = False
'''Set to True to turn on debugging mode. This enables opening IPython on
exceptions.
'''
Here is some code that will scan a module and pull out names of global variable definitions, the value and a docstring that follows.
def GetVarDocs(fname):
'''Read the module referenced in fname (often <module>.__file__) and return a
dict with global variables, their value and the "docstring" that follows
the definition of the variable
'''
import ast,os
fname = os.path.splitext(fname)[0]+'.py' # convert .pyc to .py
with open(fname, 'r') as f:
fstr = f.read()
d = {}
key = None
for node in ast.walk(ast.parse(fstr)):
if isinstance(node,ast.Assign):
key = node.targets[0].id
d[key] = [node.value.id,'']
continue
elif isinstance(node,ast.Expr) and key:
d[key][1] = node.value.s.strip()
key = None
return d
To add to to ford’s answer about Epydoc, note that PyCharm will also use a string literal as the documentation for a variable in a class:
class Fields_Obj:
DefaultValue=None
"""Get/set the default value of the data field"""
Sphinx has a built-in syntax for documenting attributes (i.e. NOT the values as @duncan describes). Examples:
#: This is module attribute
x = 42
class MyClass:
#: This is a class attribute
y = 43
You can read more in the Sphinx docs: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autoattribute
…or in this other question: How to document a module constant in Python?
Properties can have docstrings! This covers the most common use case of documenting instance variables.
class A:
def __init__(self):
self._x = 22
@property
def x(self):
"document x"
return self._x
@x.setter
def x(self, value):
self._x = value
A.x.__doc__
A lot of answers assume you want it for offline use and points to sphinx or Epydoc.
But if you want it for runtime use the answer is that is impossible to add an attribute to another attribute. So you can’t attach a doctring to variable.
When you do:
a = True
print(a.__doc__)
You’ll be getting the docstring for the bool class.
In my application I need it for plug-ins. What I do is to use an associated variable/attribute.
Something like this:
a = True
_help_a = "help for a variable"
As this looks ugly what I’m actually using are syntactic macros (take a look a macropy module). The code looks like this:
with document:
a = True
""" help for a variable """
I explain the whole idea here
Yes, you can do that!
You can actually ‘document’ lambdas and variables in a module by attaching docstrings to them.
Not properties. Not class attributes. Variables, constants and lambdas. No Sphinx, no Epydoc. No typing.annotations
. Plain old help()
. Real docstrings. In the line of
VAR = value # (apply magicka to add docstring 'This is a variable')
help(VAR) # shows 'This is a variable'
Well, sort of …
The trick is to use decorators.
You take anything that can hold a docstring (class or function), apply decorator to it, but instead of returning modified class or wrapper class as you usually do with decorators, you can just return an object of completely different class generated on the fly with a docstring attached to it.
Something like this:
# This is ugly, yes, but it's gonna be even uglier, strap in.
@decorator_generator(lambda x: x+1)
class foo: """ docstrings """
# The decorator generator returns a decorator, which, in turn, can return
# ANYTHING AT ALL, which will be assigned to `foo`. We can use that.
The types will be very messy, but, hey, who needs them anyways.
We can also do some module class magic to reattach the docs every time a new value is assigned to module attributes, thus preserving the docs on assignment.
Consider this:
# one.py
# We import sys to use the module magicka later
import sys
def documented_named(baseclass, *init_args, **init_kwargs):
def _decorator(decorated):
class _documented_class(baseclass):
def __str__(self):
return decorated.__name__
__doc__ = decorated.__doc__
def __documented__(self):
return (documented_named, baseclass, decorated)
return _documented_class(*init_args, **init_kwargs)
return _decorator
def documented(baseclass, *init_args, **init_kwargs):
def _decorator(decorated):
class _documented_class(baseclass):
__doc__ = decorated.__doc__
def __documented__(self):
return (documented, baseclass, decorated)
return _documented_class(*init_args, **init_kwargs)
return _decorator
def documented_lambda(__lambda):
def _decorator(decorated):
def _lambda(*args):
return __lambda(*args)
_lambda.__doc__ = decorated.__doc__
return _lambda
return _decorator
class nopreserve: pass
def documented_ducktype(baseclass, *init_args, **init_kwargs):
def _decorator(decorated):
class _documented_class(baseclass):
__doc__ = decorated.__doc__
def __documented__(self):
return (documented, nopreserve, decorated)
return _documented_class(*init_args, **init_kwargs)
return _decorator
# Module magix
_thismodule = sys.modules[__name__]
class _preserve_documented(_thismodule.__class__):
def __setattr__(self, attr, value):
ex = getattr(self, attr, None)
if ex is not None and hasattr(ex, '__documented__'):
decorator, baseclass, decorated = ex.__documented__()
if baseclass is nopreserve:
baseclass = value.__class__
super().__setattr__(attr, decorator(baseclass, value)(decorated))
else:
super().__setattr__(attr,value)
_thismodule.__class__ = _preserve_documented
@documented(int, 100)
class VAR1: """ DOCUMENTATION FOR VAR1 """
@documented_named(float, 7.891011)
class VAR2: """ DOCUMENTATION FOR VAR2 """
@documented_lambda(lambda x: x+1)
class LAMBDA: """ LAMBDA DOCUMENTATION PROVIDED HERE """
@documented_ducktype(int, 100)
class VAR3:
""" DOCUMENTATION FOR VAR3
Multiline, eh?
"""
Let’s test it like this:
# two.py
import one
from one import VAR2
# inspect is here for convenience only, not needed for docs to work
import inspect
def printvar(v):
print('str: ', v)
print('repr: ', repr(v))
print('docs: ', inspect.getdoc(v))
print('')
print('Here you go, documented variables and lambda:')
for v in [one.VAR1, one.VAR2, one.LAMBDA]:
printvar(v)
# And then we set vars and check again
one.VAR1 = 12345
one.VAR2 = 789
print('nnAfter setting VAR1 and VAR2 with <module>.<varname> = foo to new values:')
for v in [one.VAR1, one.VAR2]:
printvar(v)
print('Cool, the docs are preserved.nn')
print('Note that we have also preserved the types, though this is not really necessary:')
print('VAR3:')
printvar(one.VAR3)
one.VAR3 = 'VAR3 is a string now'
print('And after assignment:')
printvar(one.VAR3)
print('VAR2 imported with "from <module> import VAR2":')
printvar(VAR2)
print('However, if we set it here we will obviously loose the doc:')
VAR2 = 999
printvar(VAR2)
print('And the types are terrible:')
print('one.VAR1: ', type(one.VAR1))
print('one.VAR2: ', type(one.VAR2))
print('one.LAMBDA:', type(one.LAMBDA))
The output:
$ python two.py
Here you go, documented variables and lambda:
str: 100
repr: 100
docs: DOCUMENTATION FOR VAR1
str: VAR2
repr: 7.891011
docs: DOCUMENTATION FOR VAR2
str: <function documented_lambda.<locals>._decorator.<locals>._lambda at 0x7fa582443790>
repr: <function documented_lambda.<locals>._decorator.<locals>._lambda at 0x7fa582443790>
docs: LAMBDA DOCUMENTATION PROVIDED HERE
After setting VAR1 and VAR2 with <module>.<varname> = foo to new values:
str: 12345
repr: 12345
docs: DOCUMENTATION FOR VAR1
str: VAR2
repr: 789.0
docs: DOCUMENTATION FOR VAR2
Cool, the docs are preserved.
Note that we have also preserved the types, though this is not really necessary:
VAR3:
str: 100
repr: 100
docs: DOCUMENTATION FOR VAR3
Multiline, eh?
And after assignment:
str: VAR3 is a string now
repr: 'VAR3 is a string now'
docs: DOCUMENTATION FOR VAR3
Multiline, eh?
VAR2 imported with "from <module> import VAR2":
str: VAR2
repr: 7.891011
docs: DOCUMENTATION FOR VAR2
However, if we set it here we will obviously loose the doc:
str: 999
repr: 999
docs: int([x]) -> integer
int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments
are given. If x is a number, return x.__int__(). For floating point
numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string,
bytes, or bytearray instance representing an integer literal in the
given base. The literal can be preceded by '+' or '-' and be surrounded
by whitespace. The base defaults to 10. Valid bases are 0 and 2-36.
Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0)
4
And the types are terrible:
one.VAR1: <class 'one.documented.<locals>._decorator.<locals>._documented_class'>
one.VAR2: <class 'one.documented_named.<locals>._decorator.<locals>._documented_class'>
one.LAMBDA: <class 'function'>
And yes, you can actually import one
and get the docs with help(one.VAR1)
this way.
And I suppose Sphinx and Epydoc can handle that pretty well if they can be forced to ignore the decorator (I did NOT test that).
Note that you can also do something similar with __metaclass__
, which should look better syntax-wise, but you’ll need Python 3.9 for that.
And I should get a time machine, this WAS totally possible since the decorators were introduced.