Contribution complete example

Say you identified a new feature you want to add to the package. In this section, we will focus on how to write the source code of such a contribution so it can be seamlessly integrated in our package. For guides on our issue tracker, our development environment, process and practice, read the relevant pages of our Contributing doc.

In this example, let’s say your adding a new type of aggregator algorithm (MAVT).

You need to start by looking at other algorithms and features in the same domain (close in functionality or algorithm) already implemented, and see how they are structured. It is important such closeness in concept be mirrored by the same closeness in usage for the sake of the package ergonomy. Generally, those close already implemented features will be based on some abstract classes and internal functions/objects which should then be reused when appropriate to have a code extendable and maintainable in the long term.

There are also some modules entirely focused on organizing the features of this package. This is the case for the mcda.internal.core.interfaces module which defines some generic abstract classes used across the package (the types of MCDA algorithms for instance).

In our example, aggregators are imported for users in the mcda.mavt.aggregators module and implemented in the mcda.internal.core.aggregators module. They are all subclasses from the abstract mcda.internal.core.aggregators.Aggregator class. We can also see they precise the types of input scales and output scale on which they are defined. So our new aggregator should implement this same API and even be a subclass. They don’t implement any interface from mcda.internal.core.interfaces though.

Let’s say our aggregator takes quantitative values and aggregate them as quantitative values:

# src/mcda/internal/core/aggregators.py

...

@set_module("mcda.mavt.aggregators")
class MySuperAggregator(Aggregator[QuantitativeScale, QuantitativeScale]):
    pass
# src/mcda/mavt/aggregators.py

from ..internal.core.aggregators import MySuperAggregator, ...

__all__ = [
    ...
    "MySuperAggregator",
]

Afterwards, you can decompose the feature you want to add. It is probable that some of those subfeatures are already implemented elsewhere in the package in which case just reuse them (no need to reinvent the wheel). They may need to be made more generic in which case you should add this new generic subfeature and make the existing implementation use it (still for the sake of easier maintainability).

In our case, let’s say we don’t need many subfeatures. Also, the parent class mcda.internal.core.aggregators.Aggregator of our new aggregator implements a lot of functionality for us. The aggregator call function is defined for many kind of data structures and we only need to implement the method to aggregate pandas.Series (in some cases it may be more efficient to also override the other aggregation methods).

# src/mcda/internal/core/aggregators.py

...

@set_module("mcda.mavt.aggregators")
class MySuperAggregator(Aggregator[QuantitativeScale, QuantitativeScale]):

    def _aggregate_series(self, series: Series, *args, **kwargs) -> float:
        # implement series aggregation

Once we have implemented our feature, it is important to test and document it. We can look at how are documented the same types of features so that the new doc won’t be too different and easier to integrate. It is important to mention the research publication on which the implementation is based. They are gathered in the file doc/refs.bib and compiled in the doc page References.

For our example, we can take the mcda.mavt.aggregators.ChoquetIntegral doc and adapt it:

# src/mcda/internal/core/aggregators.py

...

@set_module("mcda.mavt.aggregators")
class MySuperAggregator(Aggregator[QuantitativeScale, QuantitativeScale]):
    """This class represents a super aggregator.

    :param in_scales: input scales (inferred from input if not provided)
    :param out_scale: output scales (inferred from output if not provided)

    .. note:: Implementation is based on :cite:p:`BIBTEX_REF`.
    """

    def _aggregate_series(self, series: Series, *args, **kwargs) -> float:
        """Return super aggregation of the input.

        :param series:
        :return:
        """
        # implement series aggregation

For the unit tests, we can look at how the unit tests of other close features are done. It is important to test exhaustively all method/function from the public API (even those inherited in case this class structure changes later).

In our example, let’s have a look at the unit tests for other aggregators. We can see that they are based on a common unittest.TestCase which implements all unit tests common to all mcda.internal.core.aggregators.Aggregator classes. We just need to subclass this test case and adapt its test attributes for our algorithm.

class MySuperAggregatorTestCase(unittest.TestCase, AggregatorTestCase):
    def setUp(self):
        self.table = PerformanceTable(...)
        self.values = Values(...)
        self.partial_values = PartialValueMatrix(...)
        self.aggregator = MySuperAggregator(...)
        self.aggregated_values = ...
        self.aggregated_table = CommensurableValues(...)
        self.aggregated_partial_values = AdjacencyValueMatrix(...)

If we had implemented more methods for our class, we should have added tests for them in this test case.

Then we should add an example usage of this aggregator in the notebooks, they are exported with the doc in Examples. In our case, we should place our example with the other MAVT/aggregator algorithms examples in the Multi Attribute Value Theory.

Then once the source code, its unit tests, doc and examples are all working, you should finally append your changes to the top of the changelog (file CHANGES.txt). And open up a merge request.