Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

python requires with alias #3957

Merged
merged 9 commits into from
Dec 3, 2018
10 changes: 9 additions & 1 deletion conans/client/graph/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
35 changes: 24 additions & 11 deletions conans/client/graph/python_requires.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
from collections import namedtuple
from contextlib import contextmanager

from conans.client.loader import parse_conanfile
from conans.client.recorder.action_recorder import ActionRecorder
from conans.model.ref import ConanFileReference
from conans.model.requires import Requirement


PythonRequire = namedtuple("PythonRequire", "conan_ref module")
PythonRequire = namedtuple("PythonRequire", "conan_ref module conanfile")


class ConanPythonRequire(object):
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:
Expand All @@ -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
22 changes: 14 additions & 8 deletions conans/client/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
"""

Expand Down
4 changes: 2 additions & 2 deletions conans/test/client/cmd/export_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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',
Expand Down
143 changes: 143 additions & 0 deletions conans/test/integration/python_build_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import unittest
from parameterized import parameterized

from conans.model.info import ConanInfo
from conans.model.ref import ConanFileReference
Expand Down Expand Up @@ -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")]})
Expand Down Expand Up @@ -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)