From 026d6a76bedd3409e5c487176edb7c1d9697bc77 Mon Sep 17 00:00:00 2001 From: wan Date: Thu, 25 Nov 2021 11:07:49 +0000 Subject: [PATCH] [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. X-original-commit: 16afaf6fa7729e71b631eb1a54119f5a8dbe97c2 Part-of: odoo/documentation#1969 --- conf.py | 20 ++- extensions/autodoc_field/__init__.py | 144 +++++++++++++++++++++ extensions/autodoc_placeholder/__init__.py | 2 + 3 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 extensions/autodoc_field/__init__.py diff --git a/conf.py b/conf.py index 820a86b00..011233fc2 100644 --- a/conf.py +++ b/conf.py @@ -84,6 +84,8 @@ else: source_read_replace_vals['ODOO_RELPATH'] = '/../' + str(odoo_sources_dirs[0]) source_read_replace_vals['ODOO_ABSPATH'] = str(odoo_dir) sys.path.insert(0, str(odoo_dir)) + import odoo.addons + odoo.addons.__path__.append(str(odoo_dir) + '/addons') if (3, 6) < sys.version_info < (3, 7): # Running odoo needs python 3.7 min but monkey patch version_info to be compatible with 3.6 sys.version_info = (3, 7, 0) @@ -106,12 +108,17 @@ else: ) odoo_dir_in_path = True +# Mapping between odoo models related to master data and the declaration of the +# data. This is used to point users to available xml_ids when giving values for +# a field with the autodoc_field extension. +model_references = { + 'res.country': 'odoo/addons/base/data/res_country_data.xml', + 'res.currency': 'odoo/addons/base/data/res_currency_data.xml', +} + # The Sphinx extensions to use, as module names. # They can be extensions coming with Sphinx (named 'sphinx.ext.*') or custom ones. extensions = [ - # Parse Python docstrings (autodoc, automodule, autoattribute directives) - 'sphinx.ext.autodoc' if odoo_dir_in_path else 'autodoc_placeholder', - # Link sources in other projects (used to build the reference doc) 'sphinx.ext.intersphinx', @@ -141,6 +148,13 @@ if odoo_dir_in_path: extensions += [ 'sphinx.ext.linkcode', 'github_link', + # Parse Python docstrings (autodoc, automodule, autoattribute directives) + 'sphinx.ext.autodoc', + 'autodoc_field', + ] +else: + extensions += [ + 'autodoc_placeholder', ] todo_include_todos = False diff --git a/extensions/autodoc_field/__init__.py b/extensions/autodoc_field/__init__.py new file mode 100644 index 000000000..f7ae798e9 --- /dev/null +++ b/extensions/autodoc_field/__init__.py @@ -0,0 +1,144 @@ +import datetime +from typing import Sequence + +from docutils.parsers.rst import directives +from docutils.parsers.rst.states import RSTState +from sphinx.domains.python import PyClasslike, PyAttribute +from sphinx.ext.autodoc import AttributeDocumenter, ClassDocumenter + +import odoo + + +nested_parse = RSTState.nested_parse +def patched_nested_parse(self, block, input_offset, node, match_titles=False, + state_machine_class=None, state_machine_kwargs=None): + match_titles = True + return nested_parse(self, block, input_offset, node, match_titles, state_machine_class, state_machine_kwargs) +RSTState.nested_parse = patched_nested_parse + + +class OdooClassDocumenter(ClassDocumenter): + objtype = 'model' + priority = 10 + ClassDocumenter.priority + option_spec = {**ClassDocumenter.option_spec, 'main': directives.flag} + + @classmethod + def can_document_member(cls, member, membername, isattr, parent): + return isinstance(member, odoo.models.MetaModel) + + def add_content(self, more_content): + sourcename = self.get_sourcename() + cls = self.object + if 'main' in self.options: + self.add_line(f".. _model-{cls._name.replace('.', '-')}:", sourcename) + self.add_line('.. py:attribute:: _name', sourcename) + self.add_line(f' :value: {cls._name}', sourcename) + self.add_line('' , sourcename) + super().add_content(more_content) + + def add_directive_header(self, sig: str) -> None: + """Add the directive header and options to the generated content.""" + sourcename = self.get_sourcename() + module = self.modname.split('addons.')[1].split('.')[0] + if 'main' in self.options: + title = f"Original definition from `{module}`" + else: + title = f"Additional fields with `{module}`" + + self.add_line(title, sourcename) + self.add_line('=' * len(title), sourcename) + self.add_line('', sourcename) + return super().add_directive_header(sig) + + +class FieldDocumenter(AttributeDocumenter): + objtype = 'field' + priority = 10 + AttributeDocumenter.priority + + @classmethod + def can_document_member(cls, member, membername, isattr, parent): + return isinstance(member, odoo.fields.Field) + + def update_annotations(self, parent): + super().update_annotations(parent) + annotation = parent.__annotations__ + attrname = self.object.name + annotation[attrname] = dict + field = self.object + if field.type == 'many2one': + annotation[attrname] = int + elif field.type in ('one2many', 'many2many'): + annotation[attrname] = Sequence[odoo.fields.Command] + elif field.type in ('selection', 'reference', 'char', 'text', 'html'): + annotation[attrname] = str + elif field.type == 'boolean': + annotation[attrname] = bool + elif field.type in ('float', 'monetary'): + annotation[attrname] = float + elif field.type == 'integer': + annotation[attrname] = int + elif field.type == 'date': + annotation[attrname] = datetime.date + elif field.type == 'datetime': + annotation[attrname] = datetime.datetime + + def add_content(self, more_content): + source_name = self.get_sourcename() + field = self.object + if field.required: + self.add_line(f":required:", source_name) + self.add_line(f":name: {field.string}", source_name) + if field.readonly: + self.add_line(f":readonly: this field is not supposed to/cannot be set manually", source_name) + if not field.store: + self.add_line(f":store: this field is there only for technical reasons", source_name) + if field.type == 'selection': + if isinstance(field.selection, (list, tuple)): + self.add_line(f":selection:", source_name) + for tech, nice in field.selection: + self.add_line(f" ``{tech}``: {nice}", source_name) + if field.type in ('many2one', 'one2many', 'many2many'): + comodel_name = field.comodel_name + string = f":comodel: :ref:`{comodel_name} `" + self.add_line(string, source_name) + reference = self.config.model_references.get(comodel_name) + if reference: + self.add_line(f":possible_values: `{reference} <{self.config.source_read_replace_vals['GITHUB_PATH']}/{reference}>`__", source_name) + if field.default: + self.add_line(f":default: {field.default(odoo.models.Model)}", source_name) + + super().add_content(more_content) + if field.help: + self.add_line('', source_name) + for line in field.help.strip().split('\n'): + self.add_line(line, source_name) + self.add_line('', source_name) + + def get_doc(self, encoding=None, ignore=None): + # only read docstring of field instance, do not fallback on field class + field = self.object + field.__doc__ = field.__dict__.get('__doc__', "") + res = super().get_doc(encoding, ignore) + return res + + +def disable_warn_missing_reference(app, domain, node): + if not ((domain and domain.name != 'std') or node['reftype'] != 'ref'): + target = node['reftarget'] + if target.startswith('model-'): + node['reftype'] = 'odoo_missing_ref' + return True + + +def setup(app): + app.add_config_value('model_references', {}, 'env') + directives.register_directive('py:model', PyClasslike) + directives.register_directive('py:field', PyAttribute) + app.add_autodocumenter(FieldDocumenter) + app.add_autodocumenter(OdooClassDocumenter) + app.connect('warn-missing-reference', disable_warn_missing_reference, priority=400) + + return { + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/extensions/autodoc_placeholder/__init__.py b/extensions/autodoc_placeholder/__init__.py index 470f53830..b6eb4f290 100644 --- a/extensions/autodoc_placeholder/__init__.py +++ b/extensions/autodoc_placeholder/__init__.py @@ -19,6 +19,8 @@ def setup(app): directives.register_directive('autodata', PlaceHolder) directives.register_directive('automethod', PlaceHolder) directives.register_directive('autoattribute', PlaceHolder) + directives.register_directive('autofield', PlaceHolder) + directives.register_directive('automodel', PlaceHolder) return { 'parallel_read_safe': True,