Repository: conan-io/conan

Issue:
[bug] [regression] conan 1.26.0 breaks python requirements with custom classes
Consider this example: [conan-issue.zip](https://github.com/conan-io/conan/files/4779320/conan-issue.zip)

It contains two conanfiles, a base conan file, which contains a base for conan file and custom extension of `conans.CMake` class, which allows some customizations of the CMake helper (better described in issue #3099), and normal conanfile that `python_requires` the base.

With Conan 1.25.2, this works as expected:

```
cd base
conan export ./conanfile.py user/testing
cd ..
conan create ./conanfile user/testing
```

With conan 1.26.0 the first `conan export` yields following error:

```
ERROR: Error loading conanfile at '/Users/dodo/Desktop/conan-issue/base/conanfile.py': Unable to load conanfile in /Users/dodo/Desktop/conan-issue/base/conanfile.py
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/imp.py", line 171, in load_source
    module = _load(spec)
  File "<frozen importlib._bootstrap>", line 696, in _load
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/dodo/Desktop/conan-issue/base/conanfile.py", line 5, in <module>
    class CMake(conans.CMake):
TypeError: function() argument 1 must be code, not str
```

Even if I export the base with conan 1.25.2 and then attempt to use derived with conan 1.26.0 I still get a crash:

```
ERROR: Error loading conanfile at '/Users/dodo/Desktop/conan-issue/conanfile.py': Error loading conanfile at '/Users/dodo/.conan/data/base/1.0.0/user/testing/export/conanfile.py': Unable to load conanfile in /Users/dodo/.conan/data/base/1.0.0/user/testing/export/conanfile.py
KeyError: base/1.0.0@user/testing

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/conans/client/loader.py", line 352, in _parse_conanfile
    loaded = imp.load_source(module_id, conan_file_path)
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/imp.py", line 171, in load_source
    module = _load(spec)
  File "<frozen importlib._bootstrap>", line 696, in _load
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/dodo/.conan/data/base/1.0.0/user/testing/export/conanfile.py", line 5, in <module>
    class CMake(conans.CMake):
TypeError: function() argument 1 must be code, not str
```
renaming new cmake build helper for toolchain
Changelog: Bugfix: Rename new toolchain build helper to ``CMakeCmd`` to allow inheritance
Docs: https://github.com/conan-io/docs/pull/XXXX

Fix https://github.com/conan-io/conan/issues/7196

#tags: slow



Discussion:
This is not because of python_requires, but because of inheriting from `conans.CMake`, as we changed the interface for the new "toolchain" feature. Proposing a new name in #7198 to avoid the issue, though we might want to consider other alternatives, as metaclasses.
Beat me to the story!  The workaround I took was
```
# In 1.26.0, conans.CMake became a factory so we need to pick the class it returns
try:
    from conans.client.build.cmake import CMakeBuildHelper as CMake
except ImportError:
    from conans import CMake
```
Thanks for the hint @rddesmond 
Don't hesitate to report these issues before implementing workarounds, when new versions introduce regressions, we are willing to revert them and provide other alternatives, but we need to know that we are breaking :) 


Thanks, @memsharded, great work on conan! s@DoDoENT beat me to writing the story (I swear I'm not shy -- I added a different one on Friday), and I found the workaround I posted while triaging.
Not with a metaclass, but overriding the `__new__` in a CMake class we can implement something that behaves like a factory function... wdyt?

![image](https://media1.tenor.com/images/d820481ef14b1cd2a16ff4e7660deb5f/tenor.gif?itemid=9239559)


```python

# This are the two build helpers we already have

class CMakeLegacy(object):
    def __init__(self, *args, **kwargs):
        print("CMakeLegacy::__init__")
        print(" - args: {}".format(args))

    def who(self):
        print("This is the LEGACY cmake")


class CMakeToolchain(object):
    def __init__(self, *args, **kwargs):
        print("CMakeToolchain::__init__")
        print(" - args: {}".format(args))

    def who(self):
        print("TOOLCHAIN for the Win!")


# This is the one exported in `form conans import CMake`

class CMake(object):
    def __new__(cls, has_toolchain):
        """ Inject the proper CMake base class in the hierarchy """
        
        # If already injected, create and return
        if CMakeToolchain in cls.__bases__ or CMakeLegacy in cls.__bases__:
            return super(CMake, cls).__new__(cls)

        # If not, add the proper CMake implementation
        if has_toolchain:
            CustomCMakeClass = type("CustomCMakeClass", (cls, CMakeToolchain), {})
        else:
            CustomCMakeClass = type("CustomCMakeClass", (cls, CMakeLegacy), {})

        return CustomCMakeClass.__new__(CustomCMakeClass, has_toolchain)

    def __init__(self, *args, **kwargs):
        super(CMake, self).__init__(*args, **kwargs)



if __name__ == "__main__":
    has_toolchain = True  # Minimal example of a conanfile

    print("-"*20)
    cmake = CMake(has_toolchain)
    cmake.who()

    print("-"*20)

    class OtherCMake(CMake):
        def __init__(self, *args, **kwargs):
            super(OtherCMake, self).__init__(*args, **kwargs)
            print("OtherCMake::__init__")

        def who(self):
            super(OtherCMake, self).who()
            print("OtherCMake::who")

    other = OtherCMake(has_toolchain)
    other.who()

```
I like the idea, but quick question, how do you inject the ``has_toolchain`` selector onto something that the user imports?
No, the `has_toolchain` is the `conanfile, *args, **kwargs` that the user is already passing to the `CMake(conanfile=self, ....)`.

Resolving patch:
diff --git a/conans/client/build/cmake.py b/conans/client/build/cmake.py
--- a/conans/client/build/cmake.py
+++ b/conans/client/build/cmake.py
@@ -22,18 +22,33 @@
 from conans.util.runners import version_runner
 
 
-def CMake(conanfile, *args, **kwargs):
-    from conans import ConanFile
-    if not isinstance(conanfile, ConanFile):
-        raise ConanException("First argument of CMake() has to be ConanFile. Use CMake(self)")
-
-    # If there is a toolchain, then use the toolchain helper one
-    toolchain = getattr(conanfile, "toolchain", None)
-    if toolchain:
+class CMake(object):
+    def __new__(cls, conanfile, *args, **kwargs):
+        """ Inject the proper CMake base class in the hierarchy """
+        from conans import ConanFile
+        if not isinstance(conanfile, ConanFile):
+            raise ConanException("First argument of CMake() has to be ConanFile. Use CMake(self)")
+
+        # If already injected, create and return
         from conans.client.build.cmake_toolchain_build_helper import CMakeToolchainBuildHelper
-        return CMakeToolchainBuildHelper(conanfile, *args, **kwargs)
-    else:
-        return CMakeBuildHelper(conanfile, *args, **kwargs)
+        if CMakeToolchainBuildHelper in cls.__bases__ or CMakeBuildHelper in cls.__bases__:
+            return super(CMake, cls).__new__(cls)
+
+        # If not, add the proper CMake implementation
+        if hasattr(conanfile, "toolchain"):
+            CustomCMakeClass = type("CustomCMakeClass", (cls, CMakeToolchainBuildHelper), {})
+        else:
+            CustomCMakeClass = type("CustomCMakeClass", (cls, CMakeBuildHelper), {})
+
+        return CustomCMakeClass.__new__(CustomCMakeClass, conanfile, *args, **kwargs)
+
+    def __init__(self, *args, **kwargs):
+        super(CMake, self).__init__(*args, **kwargs)
+
+    @staticmethod
+    def get_version():
+        # FIXME: Conan 2.0 This function is require for python2
+        return CMakeBuildHelper.get_version()
 
 
 class CMakeBuildHelper(object):
@@ -446,6 +461,3 @@ def get_version():
             return Version(version_str)
         except Exception as e:
             raise ConanException("Error retrieving CMake version: '{}'".format(e))
-
-
-CMake.get_version = CMakeBuildHelper.get_version


Test patch:
diff --git a/conans/test/functional/build_helpers/cmake_test.py b/conans/test/functional/build_helpers/cmake_test.py
--- a/conans/test/functional/build_helpers/cmake_test.py
+++ b/conans/test/functional/build_helpers/cmake_test.py
@@ -1,19 +1,79 @@
 import textwrap
 import unittest
 
-
 from conans.test.utils.tools import TestClient
 
 
 class CMakeBuildHelper(unittest.TestCase):
-    def get_version_test(self):
+    def test_get_version_no_toolchain(self):
         client = TestClient()
         conanfile = textwrap.dedent("""
         from conans import ConanFile, CMake
         class Pkg(ConanFile):
             def build(self):
                 self.output.info("CMAKE_VERSION: %s" % CMake.get_version())
+                cmake = CMake(self)
+                self.output.info("CMAKE_VERSION (obj): %s" % cmake.get_version())
         """)
         client.save({"conanfile.py": conanfile})
         client.run("create . pkg/0.1@user/testing")
         self.assertIn("CMAKE_VERSION: 3", client.out)
+        self.assertIn("CMAKE_VERSION (obj): 3", client.out)
+
+    def test_get_version_toolchain(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+        from conans import ConanFile, CMake
+        class Pkg(ConanFile):
+            toolchain = "cmake"
+
+            def build(self):
+                self.output.info("CMAKE_VERSION: %s" % CMake.get_version())
+                cmake = CMake(self)
+                self.output.info("CMAKE_VERSION (obj): %s" % cmake.get_version())
+        """)
+        client.save({"conanfile.py": conanfile})
+        client.run("create . pkg/0.1@user/testing")
+        self.assertIn("CMAKE_VERSION: 3", client.out)
+        self.assertIn("CMAKE_VERSION (obj): 3", client.out)
+
+    def test_inheriting_cmake_build_helper_no_toolchain(self):
+        # https://github.com/conan-io/conan/issues/7196
+        base = textwrap.dedent("""
+            import conans
+            class CMake(conans.CMake):
+                pass
+            class BaseConanFile(conans.ConanFile):
+                def build(self):
+                    self.output.info("CMAKE_VERSION: %s" % conans.CMake.get_version())
+                    cmake = CMake(self)
+                    self.output.info("CMAKE_VERSION (obj): %s" % cmake.get_version())
+        """)
+
+        client = TestClient()
+        client.save({"conanfile.py": base})
+        client.run("create . pkg/0.1@")
+        self.assertIn("pkg/0.1: Exported revision", client.out)
+        self.assertIn("CMAKE_VERSION: 3", client.out)
+        self.assertIn("CMAKE_VERSION (obj): 3", client.out)
+
+    def test_inheriting_cmake_build_helper_toolchain(self):
+        # https://github.com/conan-io/conan/issues/7196
+        base = textwrap.dedent("""
+            import conans
+            class CMake(conans.CMake):
+                pass
+            class BaseConanFile(conans.ConanFile):
+                toolchain = "cmake"
+                def build(self):
+                    self.output.info("CMAKE_VERSION: %s" % conans.CMake.get_version())
+                    cmake = CMake(self)
+                    self.output.info("CMAKE_VERSION (obj): %s" % cmake.get_version())
+        """)
+
+        client = TestClient()
+        client.save({"conanfile.py": base})
+        client.run("create . pkg/0.1@")
+        self.assertIn("pkg/0.1: Exported revision", client.out)
+        self.assertIn("CMAKE_VERSION: 3", client.out)
+        self.assertIn("CMAKE_VERSION (obj): 3", client.out)