Repository: conan-io/conan

Issue:
Gather information about transitive python_requires for graph output
[Here it is](https://github.com/conan-io/conan/pull/3957/files#diff-c1d8c5f67c39ed424586c4102d74afc2R712) a test for a complex dependency graph.

![image](https://user-images.githubusercontent.com/1406456/49101720-29bbbe80-f277-11e8-9402-4aad9f466723.png)


Its output is the following:

```
project/1.0@jgsogo/test: Installing package
Requirements
    project/1.0@jgsogo/test from local cache - Cache
Python requires
    python_requires2/1.0@jgsogo/test
    python_requires22/1.0@jgsogo/test
Packages
    project/1.0@jgsogo/test:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Build
```

and it should contain inside the _Python requires_ section the references `python_requires11/1.0@jgsogo/test`, `python_requires0/1.0@jgsogo/test`  and `python_requires1/1.0@jgsogo/test`, which are the _transitive python_requires_:

```
Python requires
    python_requires11/1.0@jgsogo/test
    python_requires0/1.0@jgsogo/test
    python_requires22/1.0@jgsogo/test
    python_requires1/1.0@jgsogo/test
    python_requires2/1.0@jgsogo/test
```




Discussion:

Resolving patch:
diff --git a/conans/client/graph/printer.py b/conans/client/graph/printer.py
--- a/conans/client/graph/printer.py
+++ b/conans/client/graph/printer.py
@@ -5,12 +5,20 @@
 from conans.client.graph.graph import BINARY_SKIP
 
 
+def _get_python_requires(conanfile):
+    result = set()
+    for py_require in getattr(conanfile, "python_requires", []):
+        result.add(py_require.conan_ref)
+        result.update(_get_python_requires(py_require.conanfile))
+    return result
+
+
 def print_graph(deps_graph, out):
     requires = OrderedDict()
     build_requires = OrderedDict()
     python_requires = set()
     for node in sorted(deps_graph.nodes):
-        python_requires.update(m.conan_ref for m in getattr(node.conanfile, "python_requires", []))
+        python_requires.update(_get_python_requires(node.conanfile))
         if not node.conan_ref:
             continue
         package_id = PackageReference(node.conan_ref, node.conanfile.info.package_id())
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
@@ -1,4 +1,5 @@
 from collections import namedtuple
+from contextlib import contextmanager
 
 from conans.client.loader import parse_conanfile
 from conans.client.recorder.action_recorder import ActionRecorder
@@ -6,7 +7,7 @@
 from conans.model.requires import Requirement
 
 
-PythonRequire = namedtuple("PythonRequire", "conan_ref module")
+PythonRequire = namedtuple("PythonRequire", "conan_ref module conanfile")
 
 
 class ConanPythonRequire(object):
@@ -14,15 +15,16 @@ def __init__(self, proxy, range_resolver):
         self._cached_requires = {}  # {conan_ref: PythonRequire}
         self._proxy = proxy
         self._range_resolver = range_resolver
-        self._requires = []
+        self._requires = None
 
-    @property
-    def requires(self):
-        result = self._requires
+    @contextmanager
+    def capture_requires(self):
+        old_requires = self._requires
         self._requires = []
-        return result
+        yield self._requires
+        self._requires = old_requires
 
-    def __call__(self, require):
+    def _look_for_require(self, require):
         try:
             python_require = self._cached_requires[require]
         except KeyError:
@@ -34,8 +36,19 @@ def __call__(self, require):
             result = self._proxy.get_recipe(r, False, False, remote_name=None,
                                             recorder=ActionRecorder())
             path, _, _, reference = result
-            module, _ = parse_conanfile(path)
-            python_require = PythonRequire(reference, module)
+            module, conanfile = parse_conanfile(conanfile_path=path, python_requires=self)
+
+            # Check for alias
+            if getattr(conanfile, "alias", None):
+                # Will register also the aliased
+                python_require = self._look_for_require(conanfile.alias)
+            else:
+                python_require = PythonRequire(reference, module, conanfile)
             self._cached_requires[require] = python_require
-        self._requires.append(python_require)
-        return python_require.module
+
+        return python_require
+
+    def __call__(self, require):
+        python_req = self._look_for_require(require)
+        self._requires.append(python_req)
+        return python_req.module
diff --git a/conans/client/loader.py b/conans/client/loader.py
--- a/conans/client/loader.py
+++ b/conans/client/loader.py
@@ -43,13 +43,8 @@ def __init__(self, runner, output, python_requires):
         sys.modules["conans"].python_requires = python_requires
 
     def load_class(self, conanfile_path):
-        loaded, filename = parse_conanfile(conanfile_path)
-        try:
-            conanfile = _parse_module(loaded, filename)
-            conanfile.python_requires = self._python_requires.requires
-            return conanfile
-        except Exception as e:  # re-raise with file name
-            raise ConanException("%s: %s" % (conanfile_path, str(e)))
+        _, conanfile = parse_conanfile(conanfile_path, self._python_requires)
+        return conanfile
 
     def load_export(self, conanfile_path, name, version, user, channel):
         conanfile = self.load_class(conanfile_path)
@@ -218,7 +213,18 @@ def _invalid_python_requires(require):
     raise ConanException("Invalid use of python_requires(%s)" % require)
 
 
-def parse_conanfile(conan_file_path):
+def parse_conanfile(conanfile_path, python_requires):
+    with python_requires.capture_requires() as py_requires:
+        module, filename = _parse_conanfile(conanfile_path)
+        try:
+            conanfile = _parse_module(module, filename)
+            conanfile.python_requires = py_requires
+            return module, conanfile
+        except Exception as e:  # re-raise with file name
+            raise ConanException("%s: %s" % (conanfile_path, str(e)))
+
+
+def _parse_conanfile(conan_file_path):
     """ From a given path, obtain the in memory python import module
     """
 


Test patch:
diff --git a/conans/test/client/cmd/export_test.py b/conans/test/client/cmd/export_test.py
--- a/conans/test/client/cmd/export_test.py
+++ b/conans/test/client/cmd/export_test.py
@@ -8,7 +8,7 @@
 from conans.test.utils.tools import TestServer, TestClient
 from conans.util.files import save, load
 from conans.client.cmd.export import _replace_scm_data_in_conanfile
-from conans.client.loader import parse_conanfile
+from conans.client.loader import _parse_conanfile
 from conans.test.utils.test_files import temp_folder
 from conans.model.scm import SCMData
 
@@ -48,7 +48,7 @@ def _do_actual_test(self, scm_data, after_scm, after_recipe):
         scm_data = SCMData(conanfile=namedtuple('_', 'scm')(scm=scm_data))
         _replace_scm_data_in_conanfile(self.conanfile_path, scm_data)
         self.assertEqual(load(self.conanfile_path), target_conanfile)
-        parse_conanfile(self.conanfile_path)  # Check that the resulting file is valid python code.
+        _parse_conanfile(conan_file_path=self.conanfile_path)  # Check that the resulting file is valid python code.
 
     def test_conanfile_after_scm(self):
         scm_data = {'type': 'git',
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 os
 import unittest
+from parameterized import parameterized
 
 from conans.model.info import ConanInfo
 from conans.model.ref import ConanFileReference
@@ -82,6 +83,20 @@ def package_info(self):
         client.save({"conanfile.py": conanfile})
         client.run("export . MyConanfileBase/1.1@lasote/testing")
 
+    def with_alias_test(self):
+        client = TestClient(servers={"default": TestServer()},
+                            users={"default": [("lasote", "mypass")]})
+        self._define_base(client)
+        client.run("alias MyConanfileBase/LATEST@lasote/testing MyConanfileBase/1.1@lasote/testing")
+
+        reuse = """from conans import python_requires
+base = python_requires("MyConanfileBase/LATEST@lasote/testing")
+class PkgTest(base.MyConanfileBase):
+    pass
+"""
+        client.save({"conanfile.py": reuse}, clean_first=True)
+        client.run("create . Pkg/0.1@lasote/testing")
+
     def reuse_test(self):
         client = TestClient(servers={"default": TestServer()},
                             users={"default": [("lasote", "mypass")]})
@@ -692,3 +707,131 @@ def external_baz():
         # Should work even if PYTHONPATH is not declared as [], only external resource needed
         client.run('install Hello/0.1@lasote/stable --build missing -e PYTHONPATH="%s"'
                    % external_dir)
+
+
+class PythonRequiresNestedTest(unittest.TestCase):
+
+    @parameterized.expand([(False, False), (True, False), (True, True), ])
+    def test_python_requires_with_alias(self, use_alias, use_alias_of_alias):
+        assert use_alias if use_alias_of_alias else True
+        version_str = "latest2" if use_alias_of_alias else "latest" if use_alias else "1.0"
+        client = TestClient()
+
+        # Create python_requires
+        client.save({CONANFILE: """
+from conans import ConanFile
+
+class PythonRequires0(ConanFile):
+
+    def build(self):
+        super(PythonRequires0, self).build()
+        self.output.info(">>> PythonRequires0::build (v={{}})".format(self.version))
+        """.format(v=version_str)})
+        client.run("export . python_requires0/1.0@jgsogo/test")
+        client.run("alias python_requires0/latest@jgsogo/test "
+                   "python_requires0/1.0@jgsogo/test")
+        client.run("alias python_requires0/latest2@jgsogo/test "
+                   "python_requires0/latest@jgsogo/test")
+
+        # Create python requires, that require the previous one
+        client.save({CONANFILE: """
+from conans import ConanFile, python_requires
+
+base = python_requires("python_requires0/{v}@jgsogo/test")
+
+class PythonRequires1(base.PythonRequires0):
+    def build(self):
+        super(PythonRequires1, self).build()
+        self.output.info(">>> PythonRequires1::build (v={{}})".format(self.version))
+        """.format(v=version_str)})
+        client.run("export . python_requires1/1.0@jgsogo/test")
+        client.run("alias python_requires1/latest@jgsogo/test python_requires1/1.0@jgsogo/test")
+        client.run("alias python_requires1/latest2@jgsogo/test python_requires1/latest@jgsogo/test")
+
+        # Create python requires
+        client.save({CONANFILE: """
+from conans import ConanFile, python_requires
+
+class PythonRequires11(ConanFile):
+    def build(self):
+        super(PythonRequires11, self).build()
+        self.output.info(">>> PythonRequires11::build (v={{}})".format(self.version))
+        """.format(v=version_str)})
+        client.run("export . python_requires11/1.0@jgsogo/test")
+        client.run("alias python_requires11/latest@jgsogo/test python_requires11/1.0@jgsogo/test")
+        client.run("alias python_requires11/latest2@jgsogo/test "
+                   "python_requires11/latest@jgsogo/test")
+
+        # Create python requires, that require the previous one
+        client.save({CONANFILE: """
+from conans import ConanFile, python_requires
+
+base = python_requires("python_requires0/{v}@jgsogo/test")
+
+class PythonRequires22(base.PythonRequires0):
+    def build(self):
+        super(PythonRequires22, self).build()
+        self.output.info(">>> PythonRequires22::build (v={{}})".format(self.version))
+        """.format(v=version_str)})
+        client.run("export . python_requires22/1.0@jgsogo/test")
+        client.run("alias python_requires22/latest@jgsogo/test python_requires22/1.0@jgsogo/test")
+        client.run(
+            "alias python_requires22/latest2@jgsogo/test python_requires22/latest@jgsogo/test")
+
+        # Another python_requires, that requires the previous python requires
+        client.save({CONANFILE: """
+from conans import ConanFile, python_requires
+
+base_class = python_requires("python_requires1/{v}@jgsogo/test")
+base_class2 = python_requires("python_requires11/{v}@jgsogo/test")
+
+class PythonRequires2(base_class.PythonRequires1, base_class2.PythonRequires11):
+
+    def build(self):
+        super(PythonRequires2, self).build()
+        self.output.info(">>> PythonRequires2::build (v={{}})".format(self.version))
+        """.format(v=version_str)})
+        client.run("export . python_requires2/1.0@jgsogo/test")
+        client.run("alias python_requires2/latest@jgsogo/test python_requires2/1.0@jgsogo/test")
+        client.run("alias python_requires2/latest2@jgsogo/test python_requires2/latest@jgsogo/test")
+
+        # My project, will consume the latest python requires
+        client.save({CONANFILE: """
+from conans import ConanFile, python_requires
+
+base_class = python_requires("python_requires2/{v}@jgsogo/test")
+base_class2 = python_requires("python_requires22/{v}@jgsogo/test")
+
+class Project(base_class.PythonRequires2, base_class2.PythonRequires22):
+
+    def build(self):
+        super(Project, self).build()
+        self.output.info(">>> Project::build (v={{}})".format(self.version))
+        """.format(v=version_str)})
+
+        client.run("create . project/1.0@jgsogo/test --build=missing")
+
+        # Check that everything is being built
+        self.assertIn("project/1.0@jgsogo/test: >>> PythonRequires11::build (v=1.0)", client.out)
+        self.assertIn("project/1.0@jgsogo/test: >>> PythonRequires0::build (v=1.0)", client.out)
+        self.assertIn("project/1.0@jgsogo/test: >>> PythonRequires22::build (v=1.0)", client.out)
+        self.assertIn("project/1.0@jgsogo/test: >>> PythonRequires1::build (v=1.0)", client.out)
+        self.assertIn("project/1.0@jgsogo/test: >>> PythonRequires2::build (v=1.0)", client.out)
+        self.assertIn("project/1.0@jgsogo/test: >>> Project::build (v=1.0)", client.out)
+
+        # Check that all the graph is printed properly
+        #   - requirements
+        self.assertIn("    project/1.0@jgsogo/test from local cache - Cache", client.out)
+        #   - python requires
+        self.assertIn("    python_requires11/1.0@jgsogo/test", client.out)
+        self.assertIn("    python_requires0/1.0@jgsogo/test", client.out)
+        self.assertIn("    python_requires22/1.0@jgsogo/test", client.out)
+        self.assertIn("    python_requires1/1.0@jgsogo/test", client.out)
+        self.assertIn("    python_requires2/1.0@jgsogo/test", client.out)
+        #   - packages
+        self.assertIn("    project/1.0@jgsogo/test:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Build",
+                      client.out)
+
+        #   - no mention to alias
+        self.assertNotIn("alias", client.out)
+        self.assertNotIn("alias2", client.out)