Repository: conan-io/conan

Issue:
issue inheriting 'scm' attribute with python requires
Conan version 1.14.4

I want to use python_requires to share the setting of the scm attribute across several recipes.

**pkg recipe:**
```
from conans import ConanFile, python_requires, tools

baseline = "Base/1.0.0@my-baselines/test"

# recipe inherits properties from the conanfile defined in the baseline
class ModuleConan(python_requires(baseline).get_conanfile()):
    name = "module_name"
    version = "1.0.0"
```

**base file:**
```
def get_conanfile():
    
    class BaseConanFile(ConanFile):
        scm = {
            "type": "git", 
            "url": "auto",
            "revision": "auto"
        }
    
    return BaseConanFile

class Baseline(ConanFile):
    name = "Base"
    version = "1.0.0"
```


When running `conan create . <ref>` on the pkg recipe, conan returns:
ERROR: The conanfile.py defines more than one class level 'scm' attribute



To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:
Yes. I confirmed the bug. I'm trying to open a PR and assign to next release.

Resolving patch:
diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py
--- a/conans/client/cmd/export.py
+++ b/conans/client/cmd/export.py
@@ -208,11 +208,16 @@ def _replace_scm_data_in_conanfile(conanfile_path, scm_data):
     tree = ast.parse(content)
     to_replace = []
     comments = []
+    class_line = None
+    tab_size = 4
     for i_body, item in enumerate(tree.body):
         if isinstance(item, ast.ClassDef):
             statements = item.body
+            class_line = item.lineno
             for i, stmt in enumerate(item.body):
                 if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1:
+                    line = lines[stmt.lineno - 1]
+                    tab_size = len(line) - len(line.lstrip())
                     if isinstance(stmt.targets[0], ast.Name) and stmt.targets[0].id == "scm":
                         try:
                             if i + 1 == len(statements):  # Last statement in my ClassDef
@@ -233,14 +238,24 @@ def _replace_scm_data_in_conanfile(conanfile_path, scm_data):
                         comments = [line.strip('\n') for line in replace
                                     if line.strip().startswith("#") or not line.strip()]
                         break
-    if len(to_replace) != 1:
+
+    if len(to_replace) > 1:
         raise ConanException("The conanfile.py defines more than one class level 'scm' attribute")
 
     new_text = "scm = " + ",\n          ".join(str(scm_data).split(",")) + "\n"
-    if comments:
-        new_text += '\n'.join(comments) + "\n"
-    content = content.replace(to_replace[0], new_text)
-    content = content if not headers else ''.join(headers) + content
+
+    if len(to_replace) == 0:
+        # SCM exists, but not found in the conanfile, probably inherited from superclass
+        # FIXME: This will inject the lines only the latest class declared in the conanfile
+        tmp = lines[0:class_line]
+        tmp.append("{}{}".format(" " * tab_size, new_text))
+        tmp.extend(lines[class_line:])
+        content = ''.join(tmp)
+    else:
+        if comments:
+            new_text += '\n'.join(comments) + "\n"
+        content = content.replace(to_replace[0], new_text)
+        content = content if not headers else ''.join(headers) + content
 
     remove(conanfile_path)
     save(conanfile_path, content)


Test patch:
diff --git a/conans/test/unittests/client/cmd/export_test.py b/conans/test/unittests/client/cmd/export_test.py
--- a/conans/test/unittests/client/cmd/export_test.py
+++ b/conans/test/unittests/client/cmd/export_test.py
@@ -9,7 +9,7 @@
 from conans.model.ref import ConanFileReference
 from conans.model.scm import SCMData
 from conans.test.utils.test_files import temp_folder
-from conans.test.utils.tools import TestClient, TestServer
+from conans.test.utils.tools import TestClient, TestServer, TurboTestClient
 from conans.util.files import load, save
 
 
@@ -82,6 +82,56 @@ def test_conanfile_none(self):
         after_recipe = ''
         self._do_actual_test(scm_data=scm_data, after_scm=after_scm, after_recipe=after_recipe)
 
+    def scm_from_superclass_test(self):
+        client = TurboTestClient()
+        conanfile = '''from conans import ConanFile
+
+def get_conanfile():
+    
+    class BaseConanFile(ConanFile):
+        scm = {
+            "type": "git", 
+            "url": "auto",
+            "revision": "auto"
+        }
+    
+    return BaseConanFile
+
+class Baseline(ConanFile):
+    name = "Base"
+    version = "1.0.0"
+'''
+        client.init_git_repo({"conanfile.py": conanfile}, origin_url="http://whatever.com/c.git")
+        client.run("export . conan/stable")
+        conanfile1 = """from conans import ConanFile, python_requires, tools
+
+baseline = "Base/1.0.0@conan/stable"
+
+# recipe inherits properties from the conanfile defined in the baseline
+class ModuleConan(python_requires(baseline).get_conanfile()):
+    name = "module_name"
+    version = "1.0.0"
+"""
+        conanfile2 = """from conans import ConanFile, python_requires, tools
+
+baseline = "Base/1.0.0@conan/stable"
+
+# recipe inherits properties from the conanfile defined in the baseline
+class ModuleConan(python_requires(baseline).get_conanfile()):
+    pass
+"""
+
+        for conanfile in [conanfile1, conanfile2]:
+            client.save({"conanfile.py": conanfile})
+            client.run("export . module_name/1.0.0@conan/stable")
+            self.assertIn("module_name/1.0.0@conan/stable: "
+                          "A new conanfile.py version was exported", client.out)
+            ref = ConanFileReference.loads("module_name/1.0.0@conan/stable")
+            contents = load(os.path.join(client.cache.package_layout(ref).export(),
+                                         "conanfile.py"))
+            class_str = 'class ModuleConan(python_requires(baseline).get_conanfile()):\n'
+            self.assertIn('%s    scm = {"revision":' % class_str, contents)
+
 
 class SCMUpload(unittest.TestCase):