0

I have an abstract base class Base that provides an abstract method _run() that needs to be implemented by derived classes, as well as a method run() that will call _run() and do some extra work that is common to all derived classes.

In all derived classes, I am setting the function docstring for the _run() method. As this function is not part of the public API, I want the same docstring (and function signature) to instead show up for the run() method.

Consider the following example:

import inspect
from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def _run(self):
        return

    def run(self, *args, **kwargs):
        """old_doc"""
        return self._run(*args, **kwargs)

class Derived(Base):
    def _run(self):
        """new_doc"""
        return

My initial idea was to manipulate the docstring in Base.__init__ or Base.__new__. This works to some extent, but presents a number of problems:

  1. I want to be able to override these two methods (at the very least __init__) in derived classes.
  2. This requires the class to be instantiated before the docstring is available.
  3. By setting the docstring for Base.run when instantiating the derived class, it would in fact set the docstring for all derived classes.
class Base(ABC):
    def __init__(self):
        type(self).run.__doc__ = type(self)._run.__doc__
        type(self).run.__signature__ = inspect.signature(type(self)._run)

    ...

What I am hoping for:

>>> Derived.run.__doc__
'new_doc'

What I get so far:

>>> Derived.run.__doc__
'old_doc'
>>> Derived().run.__doc__
'new_doc'

Are there any solutions to this?

jhansen
  • 1,096
  • 1
  • 8
  • 17
  • 1
    `run` and `_run` are two different functions. Why should `run.__doc__` be replaced with `_run.__doc__`? – chepner Jun 08 '20 at 12:32
  • I want to manually manipulate the docstring, as I currently do in `Base.__init__` (see above). My question is at what point in my code to best do this manipulation. – jhansen Jun 08 '20 at 12:34
  • 1
    I don't think you *should*. What does `run` do that `_run` alone does not? Why not just override `run` in the first place? – chepner Jun 08 '20 at 12:38
  • In this simplified example it doesn't do additional work, but in my actual code it does. For instance, it automatically adds parallelism and as such provides an additional argument `njobs`. As this functionality is common to all derived classes, it makes sense to provide it in the base class. – jhansen Jun 08 '20 at 13:21
  • Also, `Derived.run` is not a different object than `Base.run`; inheritance doesn't create a new "copy", but rather allows `Derived.run` to evaluate to `Base.run` when `Derived` itself doesn't define a `run` attribute. – chepner Jun 08 '20 at 13:27
  • Yes, that's why I run into problem 3. If you know any workaround (e.g. generate the docstring on the fly whenever requested?) that would be very helpful. – jhansen Jun 08 '20 at 13:30
  • I've already told you the workaround: don't change the doc string for something that hasn't itself changed. `run` should simply be documented as doing some basic setup common to all instance of `Base`, followed by class-specific behavior determined by `_run`. – chepner Jun 08 '20 at 13:31
  • I don't want to *"change the docstring for something that hasn't itself changed"*, I want to automatically generate the docstring based on the docstring of another method. Surely that is preferable to a load of code duplication. – jhansen Jun 08 '20 at 13:33

2 Answers2

0

Don't modify the docstring of Base.run; instead, document what it does: it invokes a subclass-defined method.

class Base(ABC):
    @abstractmethod
    def _run(self):
        "Must be replaced with actual code"
        return

    def run(self, *args, **kwargs):
        """Does some setup and runs self._run"""
        return self._run(*args, **kwargs)

class Derived(Base):
    def _run(self):
        """Does some work"""
        return

There is no need to generate a new docstring for Derived.run, because Derived.run and Base.run evaluate to the exact same object: the run method defined by Base. Inheritance doesn't change what Base.run does just because it is invoked from an instance of Derived rather than an instance of Base.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • I understand your reasoning for recommending not to programmatically alter the docstring. But unfortunately, this doesn't answer my question. There are various reasons why I want to generate the docstring this way – if only to make my code as user friendly as possible (so the user only has to check the docstring of one function, rather than two, to understand how to use it). – jhansen Jun 08 '20 at 17:31
0

The best workaround I have come up with is to create a decorator instead:

from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def run(self, *args, **kwargs):
        """old_doc"""
        return self._run(*args, **kwargs)

def extra_work(func):
    # Do some extra work and modify func.__doc__
    ...
    return func

class Derived(Base):
    @extra_work
    def run(self):
        """new_doc"""
        return

This way the extra work can still be defined outside the derived class to avoid duplicating it in every class derived from Base, and I am able to automatically update the docstring to reflect the added functionality.

jhansen
  • 1,096
  • 1
  • 8
  • 17