Repository: conan-io/conan

Issue:
[python-requires] re-use scm variable for building from sources
- Conan version: 1.7.1
- I'm using python_requires to re-use scm variable.
- The library builds fine, but the final package which consumes the library does not.
- This is because the scm variable is not expanded/processed while saving the conanfile for upload in local cache

conanfile for MyBase:
```
scm = {
"type" : "git",
"url" : "auto",
"revision" : "auto"
}
```

conanfile for consumer library:
```
from conans import ConanFile, CMake, tools, python_requires
base = python_requires("MyBase/1.7@demo/stable")

class MyPkgConanFile(base.MyBase):
    scm = base.scm
```
The same conanfile is saved in local cache, as opposed to the expanded values of scm variable.




- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/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, seems an issue.

Wouldn't you actually prefer that ``scm`` is an attribute of the base class, and extending the base class automatically would handle it?

EDIT: Oh, I see. If added to the base class, then it will already be evaluated when exporting the base class. And that one doesn't have any idea it will be a ``python_requires``, so it is not possible to skip it.
The origin is this line:

https://github.com/conan-io/conan/blob/8c9ea8d4f4903bfb781779056c0d79f3701c3115/conans/model/scm.py#L44

This replacement should be improved to:

- Replace also other expressions: ``scm = some-expression``. I would say to allow single-line only
- Generate it, even if ``scm = `` doesn't exist in the conanfile, for the case it is being inherited from the base conanfile.

Feedback from @lasote requested

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
@@ -1,3 +1,4 @@
+import ast
 import os
 import shutil
 
@@ -9,7 +10,7 @@
 from conans.model.manifest import FileTreeManifest
 from conans.model.scm import SCM
 from conans.paths import CONAN_MANIFEST, CONANFILE
-from conans.util.files import save, rmdir, is_dirty, set_dirty, mkdir
+from conans.util.files import save, rmdir, is_dirty, set_dirty, mkdir, load
 from conans.util.log import logger
 from conans.search.search import search_recipes
 
@@ -89,7 +90,36 @@ def _capture_export_scm_data(conanfile, conanfile_dir, destination_folder, outpu
     # Generate the scm_folder.txt file pointing to the src_path
     src_path = scm.get_repo_root()
     save(scm_src_file, src_path.replace("\\", "/"))
-    scm_data.replace_in_file(os.path.join(destination_folder, "conanfile.py"))
+    _replace_scm_data_in_conanfile(os.path.join(destination_folder, "conanfile.py"),
+                                   scm_data)
+
+
+def _replace_scm_data_in_conanfile(conanfile_path, scm_data):
+    # Parsing and replacing the SCM field
+    content = load(conanfile_path)
+    lines = content.splitlines(True)
+    tree = ast.parse(content)
+    to_replace = []
+    for item in tree.body:
+        if isinstance(item, ast.ClassDef):
+            statements = item.body
+            for i, stmt in enumerate(item.body):
+                if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1:
+                    if isinstance(stmt.targets[0], ast.Name) and stmt.targets[0].id == "scm":
+                        try:
+                            next_line = statements[i+1].lineno - 1
+                        except IndexError:
+                            next_line = stmt.lineno
+                        replace = [line for line in lines[(stmt.lineno-1):next_line]
+                                   if line.strip()]
+                        to_replace.append("".join(replace).lstrip())
+                        break
+    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"
+    content = content.replace(to_replace[0], new_text)
+    save(conanfile_path, content)
 
 
 def _export_conanfile(conanfile_path, output, paths, conanfile, conan_ref, keep_source):
diff --git a/conans/model/scm.py b/conans/model/scm.py
--- a/conans/model/scm.py
+++ b/conans/model/scm.py
@@ -2,8 +2,6 @@
 
 from conans.client.tools.scm import Git
 from conans.errors import ConanException
-from conans.util.files import load, save
-import re
 
 
 class SCMData(object):
@@ -37,13 +35,6 @@ def __repr__(self):
         d = {k: v for k, v in d.items() if v}
         return json.dumps(d, sort_keys=True)
 
-    def replace_in_file(self, path):
-        content = load(path)
-        dumps = self.__repr__()
-        dumps = ",\n          ".join(dumps.split(","))
-        content = re.sub(r"scm\s*=\s*{[^}]*}", "scm = %s" % dumps, content)
-        save(path, content)
-
 
 class SCM(object):
     def __init__(self, data, repo_folder):


Test patch:
diff --git a/conans/test/functional/scm_test.py b/conans/test/functional/scm_test.py
--- a/conans/test/functional/scm_test.py
+++ b/conans/test/functional/scm_test.py
@@ -123,6 +123,12 @@ def test_auto_git(self):
         self.assertIn("Getting sources from folder: %s" % curdir, self.client.out)
         self.assertIn("My file is copied", self.client.out)
 
+        # check blank lines are respected in replacement
+        self.client.run("get lib/0.1@user/channel")
+        self.assertIn("""}
+
+    def build(self):""", self.client.out)
+
         # Export again but now with absolute reference, so no pointer file is created nor kept
         git = Git(curdir)
         self.client.save({"conanfile.py": base.format(url=curdir, revision=git.get_revision())})
diff --git a/conans/test/integration/python_build_test.py b/conans/test/integration/python_build_test.py
--- a/conans/test/integration/python_build_test.py
+++ b/conans/test/integration/python_build_test.py
@@ -1,5 +1,6 @@
 import unittest
-from conans.test.utils.tools import TestClient, TestServer
+from conans.test.utils.tools import TestClient, TestServer,\
+    create_local_git_repo
 from conans.paths import CONANFILE, BUILD_INFO
 from conans.util.files import load, save
 import os
@@ -219,6 +220,47 @@ class PkgTest(base.MyConanfileBase):
         self.assertIn("Pkg/0.1@lasote/testing: Package installed 5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
                       client.out)
 
+    def reuse_scm_test(self):
+        client = TestClient()
+
+        conanfile = """from conans import ConanFile
+scm = {"type" : "git",
+       "url" : "somerepo",
+       "revision" : "auto"}
+
+class MyConanfileBase(ConanFile):
+    scm = scm
+"""
+        create_local_git_repo({"conanfile.py": conanfile}, branch="my_release",
+                              folder=client.current_folder)
+        client.run("export . MyConanfileBase/1.1@lasote/testing")
+        client.run("get MyConanfileBase/1.1@lasote/testing")
+        # The global scm is left as-is
+        self.assertIn("""scm = {"type" : "git",
+       "url" : "somerepo",
+       "revision" : "auto"}""", client.out)
+        # but the class one is replaced
+        self.assertNotIn("scm = scm", client.out)
+        self.assertIn('    scm = {"revision":', client.out)
+        self.assertIn('"type": "git",', client.out)
+        self.assertIn('"url": "somerepo"', client.out)
+
+        reuse = """from conans import python_requires
+base = python_requires("MyConanfileBase/1.1@lasote/testing")
+class PkgTest(base.MyConanfileBase):
+    scm = base.scm
+    other = 123
+    def _my_method(self):
+        pass
+"""
+        client.save({"conanfile.py": reuse})
+        client.run("export . Pkg/0.1@lasote/testing")
+        client.run("get Pkg/0.1@lasote/testing")
+        self.assertNotIn("scm = base.scm", client.out)
+        self.assertIn('scm = {"revision":', client.out)
+        self.assertIn('"type": "git",', client.out)
+        self.assertIn('"url": "somerepo"', client.out)
+
 
 class PythonBuildTest(unittest.TestCase):