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 :doc:`../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 :mod:`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 :mod:`mcda.mavt.aggregators` module and implemented in the :mod:`mcda.internal.core.aggregators` module. They are all subclasses from the abstract :class:`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 :mod:`mcda.internal.core.interfaces` though. Let's say our aggregator takes quantitative values and aggregate them as quantitative values: .. code:: python # src/mcda/internal/core/aggregators.py ... @set_module("mcda.mavt.aggregators") class MySuperAggregator(Aggregator[QuantitativeScale, QuantitativeScale]): pass .. code:: python # 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 :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 :class:`pandas.Series` (in some cases it may be more efficient to also override the other aggregation methods). .. code:: python # 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 :doc:`../references`. For our example, we can take the :mod:`mcda.mavt.aggregators.ChoquetIntegral` doc and adapt it: .. code:: python # 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 :class:`unittest.TestCase` which implements all unit tests common to all :class:`mcda.internal.core.aggregators.Aggregator` classes. We just need to subclass this test case and adapt its test attributes for our algorithm. .. code:: python 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 :doc:`../examples`. In our case, we should place our example with the other MAVT/aggregator algorithms examples in the :doc:`../notebooks/examples/mavt_algorithms`. 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.