Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conan/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self, method, formatters=None):
self._formatters[kind] = action
else:
raise ConanException("Invalid formatter for {}. The formatter must be"
"a valid function".format(kind))
" a valid function".format(kind))
if method.__doc__:
self._doc = method.__doc__
else:
Expand Down
130 changes: 130 additions & 0 deletions conan/cli/commands/dep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import os
import re

from conan.api.conan_api import ConanAPI
from conan.api.model import ListPattern, RecipeReference
from conan.api.output import ConanOutput
from conan.cli.command import conan_command, conan_subcommand
from conan.errors import ConanException
from conan.internal.util.files import save, load


@conan_subcommand()
def dep_remove(conan_api, parser, subparser, *args):
"""
Removes a requirement from your local conanfile.
"""
subparser.add_argument("--folder",
help="Path to a folder containing a recipe (conanfile.py). "
"Defaults to the current directory",)
subparser.add_argument("requires", nargs="*", help="Requirement name.")
subparser.add_argument("-tor", "--tool", action="append", default=[],
help="Tool requirement name.")
subparser.add_argument("-ter", "--test", action="append", default=[],
help="Test requirement name.")
args = parser.parse_args(*args)
path = conan_api.local.get_conanfile_path(args.folder or '.', os.getcwd(), py=True)
# Check if that requirement exists in the conanfile. If yes, abort.
conanfile = load(path)
ConanOutput().debug(f"Loaded conanfile from {path}.")
requires = [(r, "requires") for r in args.requires]
tool_requires = [(r, "tool_requires") for r in args.tool]
test_requires = [(r, "test_requires") for r in args.test]
success_msgs = []
for (name, req_attr) in requires + tool_requires + test_requires:
if not re.search(rf"self\.{req_attr}\([\"']{name}", conanfile):
ConanOutput().warning(f"The {req_attr} {name} is not declared in your conanfile.")
continue
# Replace the whole line
conanfile = re.sub(rf"^\s*self\.{req_attr}\([\"']{name}.*\n?", '',
conanfile, flags=re.MULTILINE)
success_msgs.append(f"Removed {name} dependency as {req_attr}.")
save(path, conanfile)
ConanOutput().success('\n'.join(success_msgs))


@conan_subcommand()
def dep_add(conan_api, parser, subparser, *args):
"""
Add a new requirement to your local conanfile as a version range.
By default, it will look for the requirement versions remotely.
"""
subparser.add_argument("--folder",
help="Path to a folder containing a recipe (conanfile.py). "
"Defaults to the current directory",)
subparser.add_argument("requires", nargs="*", help="Requirement name.")
subparser.add_argument("-tor", "--tool", action="append", default=[],
help="Tool requirement name.")
subparser.add_argument("-ter", "--test", action="append", default=[],
help="Test requirement name.")
group = subparser.add_mutually_exclusive_group()
group.add_argument("-r", "--remote", default=None, action="append",
help="Remote names. Accepts wildcards ('*' means all the remotes available)")
group.add_argument("-nr", "--no-remote", action="store_true",
help='Do not use remote, resolve exclusively in the cache')
args = parser.parse_args(*args)
requires = [(r, "requires", "requirements") for r in args.requires]
tool_requires = [(r, "tool_requires", "build_requirements") for r in args.tool]
test_requires = [(r, "test_requires", "build_requirements") for r in args.test]
if not any(requires + tool_requires + test_requires):
raise ConanException("You need to add any requires, tool_requires or test_requires.")
path = conan_api.local.get_conanfile_path(args.folder or ".", os.getcwd(), py=True)
remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [None]
conanfile = load(path)
ConanOutput().debug(f"Loaded conanfile from {path}.")
cached_results = {}
success_msgs = []
for (name, req_attr, req_func) in requires + tool_requires + test_requires:
# Check if that requirement exists in the conanfile. If yes, do nothing.
if re.search(rf"self\.{req_attr}\([\"']{name}", conanfile):
ConanOutput().warning(f"The {req_attr} {name} is already in use.")
continue
if name in cached_results:
# Avoid double-search in remotes/cache, e.g., protobuf
reference = RecipeReference.loads(f"{name}/{cached_results[name]}")
elif "/" in name: # it already brings a version
reference = RecipeReference.loads(name)
cached_results[name] = reference.version # caching the result
else: # Search the latest version in remotes/cache
ref_pattern = ListPattern(f"{name}/*")
# If neither remote nor cache are defined, show results only from cache
results = {}
for remote in remotes:
try:
pkglist = conan_api.list.select(ref_pattern, remote=remote)
except Exception as e:
remote_name = "Cache" if remote is None else remote.name
ConanOutput().warning(f"[{remote_name}] {str(e)}")
else:
results = pkglist.serialize()
if results:
break
if not results:
ConanOutput().error(f"Recipe {name} not found.")
continue
# Put the upper limit for that requirement (next major version)
reference = RecipeReference.loads(results.popitem()[0])
cached_results[name] = reference.version # caching the result
try:
version_range = f"{reference.name}/[>={reference.version} <{str(reference.version.bump(0))}]"
except ConanException: # likely cannot bump the version, using it without ranges
version_range = str(reference)
full_version_range = f'self.{req_attr}("{version_range}")'
if full_version_range:
tab_space = " " * 4
if f"def {req_func}(" in conanfile:
conanfile = conanfile.replace(f"def {req_func}(self):\n",
f"def {req_func}(self):\n{tab_space * 2}{full_version_range}\n")
else:
requirements_func = f"\n{tab_space}def {req_func}(self):\n{tab_space * 2}{full_version_range}\n"
conanfile += requirements_func
success_msgs.append(f"Added '{version_range}' as a new {req_attr}.")
save(path, conanfile)
ConanOutput().success('\n'.join(success_msgs))


@conan_command(group="Consumer")
def dep(conan_api: ConanAPI, parser, *args):
"""
Adds/removes requirements to/from your local conanfile.
"""
12 changes: 12 additions & 0 deletions conan/test/assets/genconanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(self, name=None, version=None):
self._build_requirements = None
self._tool_requires = None
self._tool_requirements = None
self._test_requirements = None
self._test_requires = None
self._revision_mode = None
self._package_info = None
Expand Down Expand Up @@ -162,6 +163,12 @@ def with_tool_requirement(self, ref, **kwargs):
self._tool_requirements.append((ref_str, kwargs))
return self

def with_test_requirement(self, ref, **kwargs):
self._test_requirements = self._test_requirements or []
ref_str = self._get_full_ref_str(ref)
self._test_requirements.append((ref_str, kwargs))
return self

def with_import(self, *imports):
for i in imports:
if i not in self._imports:
Expand Down Expand Up @@ -369,6 +376,11 @@ def _requirements_render(self):
for k, v in kwargs.items())
lines.append(' self.tool_requires("{}", {})'.format(ref, args))

for ref, kwargs in self._test_requirements or []:
args = ", ".join("{}={}".format(k, f'"{v}"' if not isinstance(v, (bool, dict)) else v)
for k, v in kwargs.items())
lines.append(' self.test_requires("{}", {})'.format(ref, args))

return "\n".join(lines)

@property
Expand Down
Empty file.
91 changes: 91 additions & 0 deletions test/integration/command/dep/test_command_dep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import textwrap

from conan.test.utils.tools import GenConanfile, TestClient


def test_add_dep():
"""
Testing the "conan dep add" command which should always use the highest version
found in the first remote server or the local cache
"""
client = TestClient(default_server_user=True, light=True)
# No conanfile is present - error
client.run("dep add hello", assert_error=True)
assert "ERROR: Conanfile not found at" in client.out
client.save({"conanfile.py": GenConanfile(name="app")})
# No requirements define
client.run("dep add", assert_error=True)
assert "ERROR: You need to add any requires, tool_requires or test_requires." in client.out
# No remote recipe "hello" exists
client.run("dep add hello")
assert "ERROR: Recipe hello not found." in client.out
hello_lib = GenConanfile(name="hello")
client.save({"hello/conanfile.py": hello_lib})
client.run("create hello --version=1.0")
client.run("create hello --version=2.0")
client.run("create hello --version=3.0")
client.run("upload * --confirm -r default")
# Save a normal requires "hello"
client.run("dep add hello")
assert "Added 'hello/[>=3.0 <4]' as a new requires." in client.out
content = client.load("conanfile.py")
assert 'self.requires("hello/[>=3.0 <4]")' in content
# Checking that it works
client.run("install .")
expected = textwrap.dedent("""\
Resolved version ranges
hello/[>=3.0 <4]: hello/3.0
""")
assert expected in client.out
# Let's add the same "hello" but now as tool_requires and test_requires
client.run("dep add --tool=hello --test=hello") # [tool|test]_requires
assert "Added 'hello/[>=3.0 <4]' as a new tool_requires." in client.out
assert "Added 'hello/[>=3.0 <4]' as a new test_requires." in client.out
# Try to add them again - does nothing and shows a warning
client.run("dep add hello --tool=hello --test=hello")
assert "The requires hello is already in use." in client.out
assert "The tool_requires hello is already in use." in client.out
assert "The test_requires hello is already in use." in client.out

# Using only the local cache
bye_lib = GenConanfile(name="bye")
client.save({"bye/conanfile.py": bye_lib})
client.run("create bye --version=1.0")
client.run("create bye --version=2.0")
client.run("dep add bye --no-remote") # from cache
assert "Added 'bye/[>=2.0 <3]' as a new requires." in client.out

# Using a specific version (it does not look for it neither locally nor remotely)
client.run("dep add mylib/1.2")
assert "Added 'mylib/[>=1.2 <2]' as a new requires." in client.out

# Using commit as a version
client.run("dep add other/cci.20203034") # can not bump the version, won't use vrange
assert "Added 'other/cci.20203034' as a new requires." in client.out


def test_remove_dep():
client = TestClient(light=True)
# No conanfile is present - error
client.run("dep remove hello", assert_error=True)
assert "ERROR: Conanfile not found at" in client.out
client.save({"conanfile.py": GenConanfile(name="app")})
# No requirement "hello" declared
client.run("dep remove hello")
assert "WARN: The requires hello is not declared in your conanfile." in client.out
client.save({"conanfile.py": GenConanfile(name="app")
.with_requirement("hello/1.2")
.with_tool_requirement("hello/1.2")
.with_test_requirement("hello/1.2")})
client.run("dep remove hello --tool=hello --test=hello")
assert "Removed hello dependency as requires." in client.out
assert "Removed hello dependency as tool_requires." in client.out
assert "Removed hello dependency as test_requires." in client.out
content = client.load("conanfile.py")
assert 'self.requires("hello/1.2"' not in content
assert 'self.tool_requires("hello/1.2"' not in content
assert 'self.test_requires("hello/1.2"' not in content
client.run("dep remove hello --tool=hello --test=hello")
assert "WARN: The requires hello is not declared in your conanfile." in client.out
assert "WARN: The tool_requires hello is not declared in your conanfile." in client.out
assert "WARN: The test_requires hello is not declared in your conanfile." in client.out
4 changes: 2 additions & 2 deletions test/integration/command/test_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def test_new_basic_template(self):

def test_new_defaults(self):
c = TestClient(light=True)
for t in ("cmake_lib", "cmake_exe", "meson_lib", "meson_exe", "msbuild_lib", "msbuild_exe",
"bazel_lib", "bazel_exe", "autotools_lib", "autotools_exe"):
for t in ("cmake_lib", "cmake_exe", "meson_lib", "meson_exe", "msbuild_lib",
"msbuild_exe", "bazel_lib", "bazel_exe", "autotools_lib", "autotools_exe"):
c.save({}, clean_first=True)
c.run(f"new {t}")
conanfile = c.load("conanfile.py")
Expand Down
Loading