|
| 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 | + } |
0 commit comments