Skip to content

Commit 3d358fd

Browse files
committed
[ADD] extensions: new autofield directive
This new directive is generating documentation from Odoo fields. This can be used to build documentation about business classes. This will help developpers import/export data and build localization modules for instance. Part-of: #1334
1 parent c37f203 commit 3d358fd

File tree

3 files changed

+163
-3
lines changed

3 files changed

+163
-3
lines changed

conf.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@
8484
source_read_replace_vals['ODOO_RELPATH'] = '/../' + str(odoo_sources_dirs[0])
8585
source_read_replace_vals['ODOO_ABSPATH'] = str(odoo_dir)
8686
sys.path.insert(0, str(odoo_dir))
87+
import odoo.addons
88+
odoo.addons.__path__.append(str(odoo_dir) + '/addons')
8789
if (3, 6) < sys.version_info < (3, 7):
8890
# Running odoo needs python 3.7 min but monkey patch version_info to be compatible with 3.6
8991
sys.version_info = (3, 7, 0)
@@ -106,12 +108,17 @@
106108
)
107109
odoo_dir_in_path = True
108110

111+
# Mapping between odoo models related to master data and the declaration of the
112+
# data. This is used to point users to available xml_ids when giving values for
113+
# a field with the autodoc_field extension.
114+
model_references = {
115+
'res.country': 'odoo/addons/base/data/res_country_data.xml',
116+
'res.currency': 'odoo/addons/base/data/res_currency_data.xml',
117+
}
118+
109119
# The Sphinx extensions to use, as module names.
110120
# They can be extensions coming with Sphinx (named 'sphinx.ext.*') or custom ones.
111121
extensions = [
112-
# Parse Python docstrings (autodoc, automodule, autoattribute directives)
113-
'sphinx.ext.autodoc' if odoo_dir_in_path else 'autodoc_placeholder',
114-
115122
# Link sources in other projects (used to build the reference doc)
116123
'sphinx.ext.intersphinx',
117124

@@ -141,6 +148,13 @@
141148
extensions += [
142149
'sphinx.ext.linkcode',
143150
'github_link',
151+
# Parse Python docstrings (autodoc, automodule, autoattribute directives)
152+
'sphinx.ext.autodoc',
153+
'autodoc_field',
154+
]
155+
else:
156+
extensions += [
157+
'autodoc_placeholder',
144158
]
145159

146160
todo_include_todos = False
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import datetime
2+
from typing import Sequence
3+
4+
from docutils.parsers.rst import directives
5+
from docutils.parsers.rst.states import RSTState
6+
from sphinx.domains.python import PyClasslike, PyAttribute
7+
from sphinx.ext.autodoc import AttributeDocumenter, ClassDocumenter
8+
9+
import odoo
10+
11+
12+
nested_parse = RSTState.nested_parse
13+
def patched_nested_parse(self, block, input_offset, node, match_titles=False,
14+
state_machine_class=None, state_machine_kwargs=None):
15+
match_titles = True
16+
return nested_parse(self, block, input_offset, node, match_titles, state_machine_class, state_machine_kwargs)
17+
RSTState.nested_parse = patched_nested_parse
18+
19+
20+
class OdooClassDocumenter(ClassDocumenter):
21+
objtype = 'model'
22+
priority = 10 + ClassDocumenter.priority
23+
option_spec = {**ClassDocumenter.option_spec, 'main': directives.flag}
24+
25+
@classmethod
26+
def can_document_member(cls, member, membername, isattr, parent):
27+
return isinstance(member, odoo.models.MetaModel)
28+
29+
def add_content(self, more_content):
30+
sourcename = self.get_sourcename()
31+
cls = self.object
32+
if 'main' in self.options:
33+
self.add_line(f".. _model-{cls._name.replace('.', '-')}:", sourcename)
34+
self.add_line('.. py:attribute:: _name', sourcename)
35+
self.add_line(f' :value: {cls._name}', sourcename)
36+
self.add_line('' , sourcename)
37+
super().add_content(more_content)
38+
39+
def add_directive_header(self, sig: str) -> None:
40+
"""Add the directive header and options to the generated content."""
41+
sourcename = self.get_sourcename()
42+
module = self.modname.split('addons.')[1].split('.')[0]
43+
if 'main' in self.options:
44+
title = f"Original definition from `{module}`"
45+
else:
46+
title = f"Additional fields with `{module}`"
47+
48+
self.add_line(title, sourcename)
49+
self.add_line('=' * len(title), sourcename)
50+
self.add_line('', sourcename)
51+
return super().add_directive_header(sig)
52+
53+
54+
class FieldDocumenter(AttributeDocumenter):
55+
objtype = 'field'
56+
priority = 10 + AttributeDocumenter.priority
57+
58+
@classmethod
59+
def can_document_member(cls, member, membername, isattr, parent):
60+
return isinstance(member, odoo.fields.Field)
61+
62+
def update_annotations(self, parent):
63+
super().update_annotations(parent)
64+
annotation = parent.__annotations__
65+
attrname = self.object.name
66+
annotation[attrname] = dict
67+
field = self.object
68+
if field.type == 'many2one':
69+
annotation[attrname] = int
70+
elif field.type in ('one2many', 'many2many'):
71+
annotation[attrname] = Sequence[odoo.fields.Command]
72+
elif field.type in ('selection', 'reference', 'char', 'text', 'html'):
73+
annotation[attrname] = str
74+
elif field.type == 'boolean':
75+
annotation[attrname] = bool
76+
elif field.type in ('float', 'monetary'):
77+
annotation[attrname] = float
78+
elif field.type == 'integer':
79+
annotation[attrname] = int
80+
elif field.type == 'date':
81+
annotation[attrname] = datetime.date
82+
elif field.type == 'datetime':
83+
annotation[attrname] = datetime.datetime
84+
85+
def add_content(self, more_content):
86+
source_name = self.get_sourcename()
87+
field = self.object
88+
if field.required:
89+
self.add_line(f":required:", source_name)
90+
self.add_line(f":name: {field.string}", source_name)
91+
if field.readonly:
92+
self.add_line(f":readonly: this field is not supposed to/cannot be set manually", source_name)
93+
if not field.store:
94+
self.add_line(f":store: this field is there only for technical reasons", source_name)
95+
if field.type == 'selection':
96+
if isinstance(field.selection, (list, tuple)):
97+
self.add_line(f":selection:", source_name)
98+
for tech, nice in field.selection:
99+
self.add_line(f" ``{tech}``: {nice}", source_name)
100+
if field.type in ('many2one', 'one2many', 'many2many'):
101+
comodel_name = field.comodel_name
102+
string = f":comodel: :ref:`{comodel_name} <model-{comodel_name.replace('.', '-')}>`"
103+
self.add_line(string, source_name)
104+
reference = self.config.model_references.get(comodel_name)
105+
if reference:
106+
self.add_line(f":possible_values: `{reference} <{self.config.source_read_replace_vals['GITHUB_PATH']}/{reference}>`__", source_name)
107+
if field.default:
108+
self.add_line(f":default: {field.default(odoo.models.Model)}", source_name)
109+
110+
super().add_content(more_content)
111+
if field.help:
112+
self.add_line('', source_name)
113+
for line in field.help.strip().split('\n'):
114+
self.add_line(line, source_name)
115+
self.add_line('', source_name)
116+
117+
def get_doc(self, encoding=None, ignore=None):
118+
# only read docstring of field instance, do not fallback on field class
119+
field = self.object
120+
field.__doc__ = field.__dict__.get('__doc__', "")
121+
res = super().get_doc(encoding, ignore)
122+
return res
123+
124+
125+
def disable_warn_missing_reference(app, domain, node):
126+
if not ((domain and domain.name != 'std') or node['reftype'] != 'ref'):
127+
target = node['reftarget']
128+
if target.startswith('model-'):
129+
node['reftype'] = 'odoo_missing_ref'
130+
return True
131+
132+
133+
def setup(app):
134+
app.add_config_value('model_references', {}, 'env')
135+
directives.register_directive('py:model', PyClasslike)
136+
directives.register_directive('py:field', PyAttribute)
137+
app.add_autodocumenter(FieldDocumenter)
138+
app.add_autodocumenter(OdooClassDocumenter)
139+
app.connect('warn-missing-reference', disable_warn_missing_reference, priority=400)
140+
141+
return {
142+
'parallel_read_safe': True,
143+
'parallel_write_safe': True,
144+
}

extensions/autodoc_placeholder/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def setup(app):
1919
directives.register_directive('autodata', PlaceHolder)
2020
directives.register_directive('automethod', PlaceHolder)
2121
directives.register_directive('autoattribute', PlaceHolder)
22+
directives.register_directive('autofield', PlaceHolder)
23+
directives.register_directive('automodel', PlaceHolder)
2224

2325
return {
2426
'parallel_read_safe': True,

0 commit comments

Comments
 (0)