Repository: conan-io/conan

Issue:
[question] Unclear how to use transitive python_requires
The documentation at https://docs.conan.io/en/latest/extending/python_requires.html#resolution-of-python-requires states that:

> python-requires can python-require other recipes too, but this should probably be limited to very few cases, we recommend to use the simplest possible structure.

But it is unclear how that should work.

I made an example where I define a base class in a python_requires module. This base class uses a helper function from another python_requires module. How can I transitively define that the base class has another python_requires? At the moment I can only get it to work by specifying all python_requires explicitly in the consumer.

Example: [transitive-python_requires.tar.gz](https://github.com/conan-io/conan/files/6035128/transitive-python_requires.tar.gz)

To run the example:
- `conan export shared-function`
- `conan export base-class`
- `conan create consumer`

It will fail with:
```
ERROR: consumer/1.0.0: Error in requirements() method, line 8
	assert self.python_requires["shared-function"].module.helper(1, 2) == 3
	ConanException: 'shared-function' is not a python_require
```

Alternatively, using the old python_requires method results in a crash.

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).


Discussion:

Resolving patch:
diff --git a/conans/client/graph/python_requires.py b/conans/client/graph/python_requires.py
--- a/conans/client/graph/python_requires.py
+++ b/conans/client/graph/python_requires.py
@@ -54,7 +54,12 @@ def __getitem__(self, item):
         try:
             return self._pyrequires[item]
         except KeyError:
-            raise ConanException("'%s' is not a python_require" % item)
+            # https://github.com/conan-io/conan/issues/8546
+            # Transitive pyrequires are accessed by inheritance derived classes
+            try:
+                return self._transitive[item]
+            except KeyError:
+                raise ConanException("'%s' is not a python_require" % item)
 
     def __setitem__(self, key, value):
         # single item assignment, direct


Test patch:
diff --git a/conans/test/integration/py_requires/python_requires_test.py b/conans/test/integration/py_requires/python_requires_test.py
--- a/conans/test/integration/py_requires/python_requires_test.py
+++ b/conans/test/integration/py_requires/python_requires_test.py
@@ -149,8 +149,8 @@ class MyConanfileBase(ConanFile):
         self.assertIn("Pkg/0.1@user/testing: My cool package!", client.out)
         self.assertIn("Pkg/0.1@user/testing: My cool package_info!", client.out)
 
-    def test_transitive_access_error(self):
-        # https://github.com/conan-io/conan/issues/5529
+    @staticmethod
+    def test_transitive_access():
         client = TestClient()
         client.save({"conanfile.py": GenConanfile()})
         client.run("export . base/1.0@user/channel")
@@ -171,18 +171,18 @@ def build(self):
                     self.python_requires["base"]
             """)
         client.save({"conanfile.py": conanfile})
-        client.run("create . pkg/0.1@user/channel", assert_error=True)
-        self.assertIn("'base' is not a python_require", client.out)
+        client.run("create . pkg/0.1@user/channel")
+        assert "pkg/0.1@user/channel: Created package" in client.out
 
         conanfile = textwrap.dedent("""
-                    from conans import ConanFile
-                    class Pkg(ConanFile):
-                        python_requires = "helper/1.0@user/channel"
-                        python_requires_extend = "base.HelloConan"
-                    """)
+            from conans import ConanFile
+            class Pkg(ConanFile):
+                python_requires = "helper/1.0@user/channel"
+                python_requires_extend = "base.HelloConan"
+            """)
         client.save({"conanfile.py": conanfile})
-        client.run("create . pkg/0.1@user/channel", assert_error=True)
-        self.assertIn("'base' is not a python_require", client.out)
+        client.run("create . pkg/0.1@user/channel")
+        assert "pkg/0.1@user/channel: Created package" in client.out
 
     def test_multiple_requires_error(self):
         client = TestClient()
@@ -853,3 +853,119 @@ def build_id(self):
         self.assertIn("Pkg/0.1@user/testing: My cool build!", client.out)
         self.assertIn("Pkg/0.1@user/testing: My cool package!", client.out)
         self.assertIn("Pkg/0.1@user/testing: My cool package_info!", client.out)
+
+
+def test_transitive_python_requires():
+    # https://github.com/conan-io/conan/issues/8546
+    client = TestClient()
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        myvar = 123
+        def myfunct():
+            return 234
+        class SharedFunction(ConanFile):
+            name = "shared-function"
+            version = "1.0"
+        """)
+    client.save({"conanfile.py": conanfile})
+    client.run("export . @user/channel")
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        class BaseClass(ConanFile):
+            name = "base-class"
+            version = "1.0"
+            python_requires = "shared-function/1.0@user/channel"
+            def build(self):
+                pyreqs = self.python_requires
+                v = pyreqs["shared-function"].module.myvar  # v will be 123
+                f = pyreqs["shared-function"].module.myfunct()  # f will be 234
+                self.output.info("%s, %s" % (v, f))
+        """)
+    client.save({"conanfile.py": conanfile})
+    client.run("export . user/channel")
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        class Consumer(ConanFile):
+            name = "consumer"
+            version = "1.0"
+            python_requires = "base-class/1.0@user/channel"
+            python_requires_extend = "base-class.BaseClass"
+        """)
+    client.save({"conanfile.py": conanfile})
+    client.run("create . @user/channel")
+    assert "consumer/1.0@user/channel: Calling build()\nconsumer/1.0@user/channel: 123, 234" in \
+           client.out
+
+
+def test_transitive_diamond_python_requires():
+    client = TestClient()
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        myvar = 123
+        def myfunct():
+            return 234
+        class SharedFunction(ConanFile):
+            name = "shared-function"
+            version = "1.0"
+        """)
+    client.save({"conanfile.py": conanfile})
+    client.run("export . @user/channel")
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        myvar = 222
+        def myfunct():
+            return 2222
+        class SharedFunction2(ConanFile):
+            name = "shared-function2"
+            version = "1.0"
+        """)
+    client.save({"conanfile.py": conanfile})
+    client.run("export . @user/channel")
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        class BaseClass(ConanFile):
+            name = "base-class"
+            version = "1.0"
+            python_requires = "shared-function/1.0@user/channel", "shared-function2/1.0@user/channel"
+            def build(self):
+                pyreqs = self.python_requires
+                v = pyreqs["shared-function"].module.myvar  # v will be 123
+                f = pyreqs["shared-function2"].module.myfunct()  # f will be 2222
+                self.output.info("%s, %s" % (v, f))
+        """)
+    client.save({"conanfile.py": conanfile})
+    client.run("export . user/channel")
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        class BaseClass2(ConanFile):
+            name = "base-class2"
+            version = "1.0"
+            python_requires = "shared-function/1.0@user/channel", "shared-function2/1.0@user/channel"
+            def package(self):
+                pyreqs = self.python_requires
+                v = pyreqs["shared-function2"].module.myvar  # v will be 222
+                f = pyreqs["shared-function"].module.myfunct()  # f will be 234
+                self.output.info("%s, %s" % (v, f))
+        """)
+    client.save({"conanfile.py": conanfile})
+    client.run("export . user/channel")
+
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        class Consumer(ConanFile):
+            name = "consumer"
+            version = "1.0"
+            python_requires = "base-class/1.0@user/channel", "base-class2/1.0@user/channel"
+            python_requires_extend = "base-class.BaseClass", "base-class2.BaseClass2"
+        """)
+    client.save({"conanfile.py": conanfile})
+    client.run("create . @user/channel")
+    assert "consumer/1.0@user/channel: Calling build()\nconsumer/1.0@user/channel: 123, 2222" in \
+           client.out
+    assert "consumer/1.0@user/channel: Calling package()\nconsumer/1.0@user/channel: 222, 234" in \
+           client.out