Repository: conan-io/conan

Issue:
Transitive python_requires does no longer work since Conan 1.17 (in combination with lockfile)
Hi @memsharded, @lasote and @jgsogo,

since Conan 1.17 there is an issue with transitive python_requires references.

To reproduce you just need the following:
1. define a simple conan file which can be used as python_requires. Let's call it "base_helper".
```python
from conans import ConanFile

def hello_world():
    message = "Hello World"
    print(message)
    return message

class MyBaseConan(ConanFile):
    name = "base_helper"
    version = "1.2.3"
    #  ..
```
2. create another simple conan file which can be used as python_requires. Let's call it "impl_helper"
```python
from conans import ConanFile, tools
from conans import python_requires #@UnresolvedImport

my_base= python_requires("base_helper/1.2.3@user/channel")
test = my_base.hello_world()

class ImplConan(ConanFile):
    name = "impl_helper"
    version = "2.3.4"
```
3. create a "normal" package recipe which python_requires "impl_helper"
```python
from conans import ConanFile, tools
from conans import python_requires #@UnresolvedImport

python_requires("impl_helper/2.3.4@user/channel")

class DummyexampleConan(ConanFile):
    name = "DummyExample"
    version = "0.0.1"
    settings = "os", "compiler", "build_type", "arch"
```

If you now call ```conan install DummyExample/0.0.1@user/channel``` and after that ```conan source``` (or something else) it will end in an error. Also ```conan graph lock DummyExample/0.0.1@test/testing``` shows the same result.

Best
Aalmann




Discussion:
Thanks very much for the detailed reproducible case. I have been able to reproduce it and working on a fix, will try to release it soon.

Resolving patch:
diff --git a/conans/model/graph_lock.py b/conans/model/graph_lock.py
--- a/conans/model/graph_lock.py
+++ b/conans/model/graph_lock.py
@@ -105,7 +105,17 @@ def __init__(self, graph=None):
                 requires = {}
                 for edge in node.dependencies:
                     requires[edge.require.ref.full_repr()] = edge.dst.id
-                python_reqs = getattr(node.conanfile, "python_requires", {})
+
+                # It is necessary to lock the transitive python-requires too, for this node
+                python_reqs = {}
+                reqs = getattr(node.conanfile, "python_requires", {})
+                while reqs:
+                    python_reqs.update(reqs)
+                    partial = {}
+                    for req in reqs.values():
+                        partial.update(getattr(req.conanfile, "python_requires", {}))
+                    reqs = partial
+
                 python_reqs = [r.ref for _, r in python_reqs.items()] if python_reqs else None
                 graph_node = GraphLockNode(node.pref if node.ref else None, python_reqs,
                                            node.conanfile.options.values, False, requires)


Test patch:
diff --git a/conans/test/functional/python_requires/python_requires_test.py b/conans/test/functional/python_requires/python_requires_test.py
--- a/conans/test/functional/python_requires/python_requires_test.py
+++ b/conans/test/functional/python_requires/python_requires_test.py
@@ -7,6 +7,7 @@
 
 from conans.model.ref import ConanFileReference
 from conans.paths import CONANFILE
+from conans.util.files import load
 from conans.test.utils.conanfile import TestConanFile
 from conans.test.utils.tools import TestClient, TestServer, \
     NO_SETTINGS_PACKAGE_ID, create_local_git_repo
@@ -187,7 +188,7 @@ def my_print():
         self.assertIn("ConanException: Invalid use of python_requires"
                       "(MyConanfileBase/1.0@lasote/testing)", client.out)
 
-    def transitive_multiple_reuse_test(self):
+    def multiple_reuse_test(self):
         client = TestClient()
         conanfile = """from conans import ConanFile
 class SourceBuild(ConanFile):
@@ -222,6 +223,40 @@ class MyConanfileBase(source.SourceBuild, package.PackageInfo):
         self.assertIn("Pkg/0.1@lasote/testing: My cool package!", client.out)
         self.assertIn("Pkg/0.1@lasote/testing: My cool package_info!", client.out)
 
+    def transitive_py_requires_test(self):
+        # https://github.com/conan-io/conan/issues/5529
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+            class Base(ConanFile):
+                pass
+            """)
+        client.save({"conanfile.py": conanfile})
+        client.run("export . base/1.0@user/channel")
+
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile, python_requires
+            py_req = python_requires("base/1.0@user/channel")
+            class PackageInfo(ConanFile):
+                pass
+            """)
+        client.save({"conanfile.py": conanfile})
+        client.run("export . helper/1.0@user/channel")
+
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile, python_requires
+            source = python_requires("helper/1.0@user/channel")
+            class MyConanfileBase(ConanFile):
+                pass
+            """)
+        client.save({"conanfile.py": conanfile})
+        client.run("install . pkg/0.1@user/channel")
+        lockfile = load(os.path.join(client.current_folder, "conan.lock"))
+        self.assertIn("base/1.0@user/channel#e41727b922c6ae54b216a58442893f3a", lockfile)
+        self.assertIn("helper/1.0@user/channel#98457e1f8d9174ed053747634ce0ea1a", lockfile)
+        client.run("source .")
+        self.assertIn("conanfile.py (pkg/0.1@user/channel): Configuring sources in", client.out)
+
     def multiple_requires_error_test(self):
         client = TestClient()
         conanfile = """from conans import ConanFile