From f13f786a35f07c6eb00f7d6d18c07a2c16664f7a Mon Sep 17 00:00:00 2001
From: Robin De Schepper <robin.deschepper93@gmail.com>
Date: Mon, 20 Sep 2021 20:54:39 +0200
Subject: [PATCH] Fixed MRO and code duplication in `setup.py` (#1672)

The duplication arose from slightly more complicated composition than usual but could be resolved by proper MRO, it took me a few passes from its original form as well before I figured out the situation is not as complicated as it seems.

`super()` is equal to `super(cls, self)` where `cls` is obtained from the current function's class scope. If arg 2  is an object it will start an MRO search of the object, but skipping classes before the 1st arg type is encountered. In our case with the following base classes: `(_command_template, base_command_class)` this means we can define our base logic in `_command_template` and use `super()` calls there to traverse the MRO in `(base_command_class,)`, so by simply defining `class install_command(_command_template, install)` and `bdist_wheel_command(_command_template, bdist_wheel)` our MRO issue is solved and the duplication removed.
---
 setup.py | 80 +++++++++++++++++++++++++++-----------------------------
 1 file changed, 38 insertions(+), 42 deletions(-)

diff --git a/setup.py b/setup.py
index 062000ca..71b0e0fa 100644
--- a/setup.py
+++ b/setup.py
@@ -62,15 +62,36 @@ def check_cmake():
     except OSError:
         return False
 
-# Extend the command line options available to the install phase.
-# These arguments must come after `install` on the command line, e.g.:
-#    python3 setup.py install --mpi --arch=skylake
-#    pip3 install --install-option '--mpi' --install-option '--arch=skylake' .
-class install_command(install):
-    user_options = install.user_options + user_options_
+
+class _command_template:
+    """
+    Override a setuptools-like command to augment the command line options.
+    Needs to appear before the command class in the class's argument list for
+    correct MRO.
+
+    Examples
+    --------
+
+    .. code-block: python
+
+      class install_command(_command_template, install):
+          pass
+
+
+      class complex_command(_command_template, mixin1, install):
+          def initialize_options(self):
+              # Both here and in `mixin1`, a `super` call is required
+              super().initialize_options()
+              # ...
+
+    """
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+        cls.user_options = super().user_options + user_options_
+
 
     def initialize_options(self):
-        install.initialize_options(self)
+        super().initialize_options()
         self.mpi  = None
         self.gpu  = None
         self.arch = None
@@ -79,7 +100,7 @@ class install_command(install):
         self.sysdeps = None
 
     def finalize_options(self):
-        install.finalize_options(self)
+        super().finalize_options()
 
     def run(self):
         # The options are stored in global variables:
@@ -98,47 +119,22 @@ class install_command(install):
         #             By default use bundled libs.
         opt['bundled'] = self.sysdeps is None
 
-        install.run(self)
+        super().run()
+
+
+class install_command(_command_template, install):
+    pass
 
 if WHEEL_INSTALLED:
-    class bdist_wheel_command(bdist_wheel):
-        user_options = bdist_wheel.user_options + user_options_
-
-        def initialize_options(self):
-            bdist_wheel.initialize_options(self)
-            self.mpi  = None
-            self.gpu  = None
-            self.arch = None
-            self.vec  = None
-            self.neuroml = None
-            self.sysdeps = None
-
-        def finalize_options(self):
-            bdist_wheel.finalize_options(self)
-
-        def run(self):
-            # The options are stored in global variables:
-            opt = cl_opt()
-            #   mpi  : build with MPI support (boolean).
-            opt['mpi']  = self.mpi is not None
-            #   gpu  : compile for AMD/NVIDIA GPUs and choose compiler (string).
-            opt['gpu']  = "none" if self.gpu is None else self.gpu
-            #   vec  : generate SIMD vectorized kernels for CPU micro-architecture (boolean).
-            opt['vec']  = self.vec is not None
-            #   arch : target CPU micro-architecture (string).
-            opt['arch'] = 'none' if self.arch is None else self.arch
-            #   neuroml : compile with neuroml support for morphologies.
-            opt['neuroml'] = self.neuroml is not None
-            #   bundled : use bundled/git-submoduled 3rd party libraries.
-            #             By default use bundled libs.
-            opt['bundled'] = self.sysdeps is None
-
-            bdist_wheel.run(self)
+    class bdist_wheel_command(_command_template, bdist_wheel):
+        pass
+
 
 class cmake_extension(Extension):
     def __init__(self, name):
         Extension.__init__(self, name, sources=[])
 
+
 class cmake_build(build_ext):
     def run(self):
         if not check_cmake():
-- 
GitLab