diff --git a/conan/api/subapi/local.py b/conan/api/subapi/local.py index dbcc4ead3b0..fd597897db0 100644 --- a/conan/api/subapi/local.py +++ b/conan/api/subapi/local.py @@ -102,7 +102,7 @@ def editable_list(self): return self._helpers.editable_packages.edited_refs def source(self, path, name=None, version=None, user=None, channel=None, - remotes: List[Remote] = None): + remotes: List[Remote] = None, lockfile=None): """ Calls the ``source()`` method of the current (user folder) ``conanfile.py`` This method does not require computing a dependency graph, because the ``source()`` @@ -114,10 +114,11 @@ def source(self, path, name=None, version=None, user=None, channel=None, :param user: The user of the package. If not defined, it is taken from conanfile :param channel: The channel of the package. If not defined, it is taken from conanfile :param remotes: The remotes to resolve possible ``python-requires`` for this recipe if needed. + :param lockfile: The lockfile to use for the ``python-requires`` resolution, if needed. """ loader = self._helpers.loader conanfile = loader.load_consumer(path, name=name, version=version, - user=user, channel=channel, graph_lock=None, + user=user, channel=channel, graph_lock=lockfile, remotes=remotes) # This profile is empty, but with the conf from global.conf profile = self._conan_api.profiles.get_profile([]) diff --git a/conan/cli/commands/source.py b/conan/cli/commands/source.py index 4a694964b74..61dfdd6dca1 100644 --- a/conan/cli/commands/source.py +++ b/conan/cli/commands/source.py @@ -1,6 +1,6 @@ import os -from conan.cli.command import conan_command +from conan.cli.command import conan_command, OnceArgument from conan.cli.args import add_reference_args @@ -12,12 +12,20 @@ def source(conan_api, parser, *args): parser.add_argument("path", help="Path to a folder containing a conanfile.py. " "Defaults to current directory", default=".", nargs="?") + parser.add_argument("-l", "--lockfile", action=OnceArgument, + help="Path to a lockfile for python-requires resolution. Use " + "--lockfile=\"\" to avoid automatic use of existing 'conan.lock' file") + parser.add_argument("--lockfile-partial", action="store_true", + help="Do not raise an error if some dependency is not found in lockfile") add_reference_args(parser) args = parser.parse_args(*args) cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=True) enabled_remotes = conan_api.remotes.list() # for python_requires not local - # TODO: Missing lockfile for python_requires + lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, + conanfile_path=path, + cwd=cwd, + partial=args.lockfile_partial) conan_api.local.source(path, name=args.name, version=args.version, user=args.user, - channel=args.channel, remotes=enabled_remotes) + channel=args.channel, remotes=enabled_remotes, lockfile=lockfile) diff --git a/test/integration/lockfile/test_lock_pyrequires.py b/test/integration/lockfile/test_lock_pyrequires.py index cf1ba0d698b..2f5b46c4f97 100644 --- a/test/integration/lockfile/test_lock_pyrequires.py +++ b/test/integration/lockfile/test_lock_pyrequires.py @@ -40,6 +40,39 @@ class MyConanfileBase(ConanFile): assert "dep/0.1" not in client.out +def test_source_should_respect_lockfile_pyrequires(): + # "conan source" currently does not resolve python_requires against a lockfile the same way + # "install"/"create"/"export"/"build" do + client = TestClient(light=True) + consumer = textwrap.dedent(""" + from conan import ConanFile + class Consumer(ConanFile): + python_requires = "dep/[>0.0]@user/channel" + def source(self): + v = self.python_requires["dep"].ref.version + self.output.info("SOURCE DEP VERSION: {}".format(v)) + """) + client.save({"dep/conanfile.py": GenConanfile(), + "conanfile.py": consumer}) + + client.run("export dep --name=dep --version=0.1 --user=user --channel=channel") + client.run("lock create conanfile.py") # writes conan.lock, locks dep/0.1 + lockfile = os.path.join(client.current_folder, "conan.lock") + assert os.path.isfile(lockfile) # the lock is indeed present when "source" runs below + + client.run("export dep --name=dep --version=0.2 --user=user --channel=channel") + + # "install" auto-discovers "conan.lock" and correctly keeps resolving dep/0.1 + client.run("install conanfile.py") + assert "dep/0.1@user/channel" in client.out + assert "dep/0.2" not in client.out + + # "source" should behave the same way and also resolve the locked dep/0.1 + client.run("source conanfile.py") + # This fails, it currently resolves to dep/0.2 + assert "SOURCE DEP VERSION: 0.1" in client.out + + def test_transitive_matching_ranges(): client = TestClient(light=True) tool = textwrap.dedent("""