Source code for spine.utils.docstring

"""Docstring inheritance utilities."""


[docs] def merge_ancestor_docstrings(cls): """Automatically merge Attributes sections from parent class docstrings. This function extracts the Attributes section from all direct parent classes and prepends them to the child class's Attributes section. This is designed to work with numpy-style docstrings. The function only looks at direct parent classes (cls.__bases__) rather than the full MRO to avoid duplicate attributes when multiple levels of inheritance are involved. Parameters ---------- cls : type The class whose docstring should be updated with parent attributes Notes ----- This function modifies the class's __doc__ attribute in-place. It is typically called from __init_subclass__ hooks in base classes to automatically merge docstrings for all subclasses. The function looks for the "Attributes" section in numpy-style docstrings, which is formatted as: Attributes ---------- attribute_name : type Description Examples -------- >>> class Parent: ... '''Parent class. ... ... Attributes ... ---------- ... x : int ... Parent attribute ... ''' ... def __init_subclass__(cls, **kwargs): ... super().__init_subclass__(**kwargs) ... merge_ancestor_docstrings(cls) ... >>> class Child(Parent): ... '''Child class. ... ... Attributes ... ---------- ... y : int ... Child attribute ... ''' >>> # Child.__doc__ now contains both x and y in Attributes section """ # Skip if the class has no docstring if cls.__doc__ is None: return # Numpy-style docstring format (no indentation on section headers) header = "Attributes\n----------\n" # Collect Attributes sections from DIRECT parent classes only # (indirect parents are already merged into direct parents) parent_attrs = [] for base in cls.__bases__: # Only direct parents, not full MRO if base is object: continue if not hasattr(base, "__doc__") or base.__doc__ is None: continue # Extract Attributes section from parent docstring if header not in base.__doc__: continue # Find the first occurrence of the header header_pos = base.__doc__.find(header) attr_start = header_pos + len(header) # Find where this section ends (next section or end of docstring) rest = base.__doc__[attr_start:] lines = rest.split("\n") attr_lines = [] for line in lines: # Stop at next section (line with only dashes, no indentation) stripped = line.strip() if ( stripped and stripped == "-" * len(stripped) and not line.startswith(" ") ): # This is a section divider, stop here break attr_lines.append(line) # Clean up and only keep non-empty content section_text = "\n".join(attr_lines).rstrip() if section_text.strip(): # Only add if there's actual content parent_attrs.append(section_text) # Now handle the child's docstring if header in cls.__doc__: # Split on FIRST occurrence only header_pos = cls.__doc__.find(header) before_header = cls.__doc__[:header_pos] after_header = cls.__doc__[header_pos + len(header) :] # Merge parent attributes before child attributes if parent_attrs: merged_parents = "\n".join(parent_attrs) + "\n" cls.__doc__ = before_header + header + merged_parents + after_header else: # No Attributes section in child, add one with just parent attrs if parent_attrs: merged_parents = "\n".join(parent_attrs) cls.__doc__ += f"\n\n{header}{merged_parents}\n"