From 47f5f1ebc4170e394372af094637acafc655b2c4 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Thu, 20 Feb 2025 17:01:15 +0100 Subject: [PATCH 01/12] Fix lint --- tests/test_conda_lock.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_conda_lock.py b/tests/test_conda_lock.py index b25cebaa6..4bdef3feb 100644 --- a/tests/test_conda_lock.py +++ b/tests/test_conda_lock.py @@ -2176,9 +2176,7 @@ def test_install( package = "tzcode" platform = "linux-64" - lock_filename_template = ( - request.node.name + "conda-{platform}.lock" - ) + lock_filename_template = request.node.name + "conda-{platform}.lock" if kind == "env": lock_filename = request.node.name + "conda-linux-64.lock.yml" elif kind == "explicit": From 7fe82a59af2e6302bea0d744eebf43ede1265ae1 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Thu, 20 Feb 2025 17:15:12 +0100 Subject: [PATCH 02/12] Add --category to install and render subcommands --- conda_lock/conda_lock.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda_lock/conda_lock.py b/conda_lock/conda_lock.py index 20b0617cb..500d2294c 100644 --- a/conda_lock/conda_lock.py +++ b/conda_lock/conda_lock.py @@ -1519,6 +1519,7 @@ def lock( @click.option( "-E", "--extras", + "--category", multiple=True, default=[], help="include extra dependencies from the lockfile (where applicable)", @@ -1643,6 +1644,7 @@ def install( @click.option( "-e", "--extras", + "--category", default=[], type=str, multiple=True, From fa3c44765e9e74dc8e02505b66abb642dfd8b9d5 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Thu, 20 Feb 2025 17:20:57 +0100 Subject: [PATCH 03/12] Add -e alias to conda-lock install for consistency AFAICT the uppercase -E was simply a typo. It was part of a massive PR, so my guess is that this just slipped through code review. --- conda_lock/conda_lock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conda_lock/conda_lock.py b/conda_lock/conda_lock.py index 500d2294c..73c5b0d03 100644 --- a/conda_lock/conda_lock.py +++ b/conda_lock/conda_lock.py @@ -1518,6 +1518,7 @@ def lock( ) @click.option( "-E", + "-e", "--extras", "--category", multiple=True, From ba7e12370e37930252146077753d1fc820063ddf Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Thu, 20 Feb 2025 17:39:34 +0100 Subject: [PATCH 04/12] Deprecate the inconsistent capital E option in conda-lock install --- conda_lock/conda_lock.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/conda_lock/conda_lock.py b/conda_lock/conda_lock.py index 73c5b0d03..50a59a142 100644 --- a/conda_lock/conda_lock.py +++ b/conda_lock/conda_lock.py @@ -1457,6 +1457,15 @@ def lock( DEFAULT_INSTALL_OPT_LOCK_FILE = pathlib.Path(DEFAULT_LOCKFILE_NAME) +def _deprecated_capital_e_callback( + ctx: click.Context, param: click.Parameter, value: Any +) -> Any: + """A click callback function raising a deprecation warning for -E.""" + if "-E" in sys.argv and value: + warn("The -E option is deprecated. Use --category or -e instead.") + return value + + @main.command("install", context_settings=CONTEXT_SETTINGS) @click.option( "--conda", @@ -1524,6 +1533,7 @@ def lock( multiple=True, default=[], help="include extra dependencies from the lockfile (where applicable)", + callback=_deprecated_capital_e_callback, ) @click.option( "--force-platform", From dee480df3934338f9659b43650c44e30503cec80 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Thu, 20 Feb 2025 18:11:38 +0100 Subject: [PATCH 05/12] Recommend --category over --extra The --category flag is compatible with micromamba. Also it's not so PyPI-centric like "extras". --- conda_lock/conda_lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda_lock/conda_lock.py b/conda_lock/conda_lock.py index 50a59a142..42a49352b 100644 --- a/conda_lock/conda_lock.py +++ b/conda_lock/conda_lock.py @@ -1061,7 +1061,7 @@ def _deprecated_dev_cli(ctx: click.Context, param: click.Parameter, value: Any) if value: raise click.BadParameter( "--dev-dependencies/--no-dev-dependencies (lock, render) and --dev/--no-dev (install) " - "switches are deprecated. Use `--extra dev` instead." + "switches are deprecated. Use `--category dev` instead." ) else: return value From b580f764268a17f870f62fd5d0aa89b862a44445 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Thu, 20 Feb 2025 18:15:03 +0100 Subject: [PATCH 06/12] When using --dev, warn instead of raise, and inject suggestion --- conda_lock/conda_lock.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/conda_lock/conda_lock.py b/conda_lock/conda_lock.py index 42a49352b..fda574d37 100644 --- a/conda_lock/conda_lock.py +++ b/conda_lock/conda_lock.py @@ -1057,14 +1057,16 @@ def _detect_lockfile_kind(path: pathlib.Path) -> TKindAll: def _deprecated_dev_cli(ctx: click.Context, param: click.Parameter, value: Any) -> Any: - """A click callback function raising a deprecation error.""" + """Raise a deprecation warning and inject `dev` into categories.""" if value: - raise click.BadParameter( + warn( "--dev-dependencies/--no-dev-dependencies (lock, render) and --dev/--no-dev (install) " "switches are deprecated. Use `--category dev` instead." ) - else: - return value + ctx.params.setdefault("extras", []) + if "dev" not in ctx.params["extras"]: + ctx.params["extras"].append("dev") + return value def handle_no_specified_source_files( From a07c77abad37558a6826339c8d8536d0ac15155a Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Fri, 21 Feb 2025 12:39:26 +0100 Subject: [PATCH 07/12] Add regression test for dev dependencies --- tests/test_regression.py | 213 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 211 insertions(+), 2 deletions(-) diff --git a/tests/test_regression.py b/tests/test_regression.py index 792d15b0e..fcf9598f7 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -1,16 +1,22 @@ """This is a test module to ensure that the various changes we've made over time don't break the functionality of conda-lock. This is a regression test suite.""" +import io +import itertools +import logging import shutil import sys import textwrap from pathlib import Path -from typing import List, Union +from textwrap import dedent +from typing import List, Optional, Union import pytest -from conda_lock.conda_lock import run_lock +from click.testing import CliRunner + +from conda_lock.conda_lock import main, run_lock from conda_lock.invoke_conda import is_micromamba from conda_lock.lookup import DEFAULT_MAPPING_URL from conda_lock.models.lock_spec import VersionedDependency @@ -122,3 +128,206 @@ def test_pip_environment_regression_gh449(pip_environment_regression_gh449: Path version="==1.10.10", ) ] + + +@pytest.fixture +def categories_environment_files(tmp_path: Path) -> List[Path]: + """Create test environment files with dependencies in different categories. + + We set up three environment files corresponding to three categories: + - main containing tzcode + - dev containing pixi + - mm containing micromamba + """ + # Main environment file (no category specified = main) + main_content = """ + channels: + - conda-forge + dependencies: + - tzcode + """ + main_file = tmp_path / "environment.yml" + main_file.write_text(textwrap.dedent(main_content)) + + # Dev environment file + dev_content = """ + channels: + - conda-forge + category: dev + dependencies: + - pixi + """ + dev_file = tmp_path / "environment-dev.yml" + dev_file.write_text(textwrap.dedent(dev_content)) + + # Custom extra environment file + mm_content = """ + channels: + - conda-forge + category: mm + dependencies: + - micromamba + """ + mm_file = tmp_path / "environment-mm.yml" + mm_file.write_text(textwrap.dedent(mm_content)) + + return [main_file, dev_file, mm_file] + + +dev_deps_and_extras_cli_regression = list( + itertools.product( + [None, True, False], # dev_deps + [False, True], # filter_cats + [[], ["dev"], ["mm"], ["dev", "mm"]], # extras + ) +) + + +def make_dev_deps_and_extras_cli_regression_id( + dev_deps: Optional[bool], filter_cats: bool, extras: List[str] +) -> str: + dev = ( + "" + if dev_deps is None + else "--dev-dependencies" + if dev_deps + else "--no-dev-dependencies" + ) + filter = "--filter-categories" if filter_cats else "" + extra = "--category=" + ",".join(extras) if extras else "" + nonempty_args = [arg for arg in [dev, filter, extra] if arg] + return "_".join(nonempty_args) or "no_args" + + +@pytest.mark.parametrize( + "dev_deps,filter_cats,extras", + dev_deps_and_extras_cli_regression, + ids=[ + make_dev_deps_and_extras_cli_regression_id(d, f, e) + for d, f, e in dev_deps_and_extras_cli_regression + ], +) +def test_dev_deps_and_extras_cli_regression( + monkeypatch: "pytest.MonkeyPatch", + categories_environment_files: List[Path], + mamba_exe: Path, + capsys: "pytest.CaptureFixture[str]", + dev_deps: Optional[bool], + filter_cats: bool, + extras: List[str], +): + """Test conda-lock's handling of dev dependencies, category filtering, and extras. + + This test verifies: + 1. The {dev-dependencies} template variable in filenames correctly reflects the dev + dependencies setting: + - "true" when dev_deps is None (default) or True + - "false" when dev_deps is False + + 2. Package inclusion based on categories and CLI options: + - Main category (tzcode) is always included + - Dev category (pixi) is included when: + * dev_deps is None or True (default behavior), or + * "dev" is in extras + - Custom category (micromamba) is included only when "mm" is in extras + + 3. File generation: + - Exactly one output file is generated + - Output filename correctly uses the {dev-dependencies} template variable + + Test Parameters: + dev_deps: Controls --dev-dependencies flag + None: Default behavior (same as True) + True: --dev-dependencies + False: --no-dev-dependencies + + filter_cats: Controls --filter-categories flag + True: Enable category filtering + False: Default behavior + + extras: Controls which extra categories to include via --category + []: No extras + ["dev"]: Include dev category + ["mm"]: Include custom category + ["dev", "mm"]: Include both categories + + The test matrix covers all combinations of these parameters (24 test cases) + to ensure consistent behavior across different CLI option combinations. + """ + # Create output directory + output_dir = categories_environment_files[0].parent / "output" + output_dir.mkdir() + + # Create a filename template using the {dev-dependencies} variable + filename_template = "conda-lock-{dev-dependencies}.lock" + + # Build the command arguments + args = [ + "lock", + "--conda", + str(mamba_exe), + "-p", + "linux-64", + "-k", + "explicit", + "--filename-template", + filename_template, + ] + + # Add all environment files + for env_file in categories_environment_files: + args.extend(["-f", str(env_file)]) + + # Add optional arguments based on the test case + if dev_deps is not None: + args.append("--dev-dependencies" if dev_deps else "--no-dev-dependencies") + if filter_cats: + args.append("--filter-categories") + for extra in extras: + args.extend(["--category", extra]) + + # Run the command from the output directory + monkeypatch.chdir(output_dir) + runner = CliRunner(mix_stderr=False) + with capsys.disabled(): + result = runner.invoke(main, args, catch_exceptions=False) + print(result.stdout, file=sys.stdout) + print(result.stderr, file=sys.stderr) + assert result.exit_code == 0 + + # Verify exactly one output file was generated + output_files = list(output_dir.glob("*")) + assert ( + len(output_files) == 1 + ), f"Expected exactly one output file, found {len(output_files)}" + output_file = output_files[0] + + # Verify the filename matches the expected dev-dependencies value + expected_dev_str = "true" if dev_deps in (None, True) else "false" + expected_filename = f"conda-lock-{expected_dev_str}.lock" + assert ( + output_file.name == expected_filename + ), f"Expected filename {expected_filename}, got {output_file.name}" + + # Read the file contents + content = output_file.read_text() + assert "tzcode" in content, "Main category dependency should always be present" + + # Check for dev category dependency + should_have_dev_category = ( + (dev_deps is None or dev_deps is True) # dev_dependencies is True by default + or "dev" in extras + ) + does_have_dev_category = "pixi" in content + assert does_have_dev_category == should_have_dev_category, ( + f"Dev category in lockfile: {does_have_dev_category}, " + f"Expected: {should_have_dev_category}" + ) + + # Check for custom extra category dependency + should_have_mm_category = "mm" in extras + does_have_mm_category = "micromamba" in content + assert does_have_mm_category == should_have_mm_category, ( + f"Custom category (mm) in lockfile: {does_have_mm_category}, " + f"Expected: {should_have_mm_category}" + ) From 3acae1494786a809e1161ffb4c30efc73e37dd28 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sun, 23 Feb 2025 09:49:40 +0100 Subject: [PATCH 08/12] Rename _deprecated_dev_cli_callback --- conda_lock/conda_lock.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/conda_lock/conda_lock.py b/conda_lock/conda_lock.py index fda574d37..bc5be619f 100644 --- a/conda_lock/conda_lock.py +++ b/conda_lock/conda_lock.py @@ -1056,7 +1056,9 @@ def _detect_lockfile_kind(path: pathlib.Path) -> TKindAll: ) -def _deprecated_dev_cli(ctx: click.Context, param: click.Parameter, value: Any) -> Any: +def _deprecated_dev_cli_callback( + ctx: click.Context, param: click.Parameter, value: Any +) -> Any: """Raise a deprecation warning and inject `dev` into categories.""" if value: warn( @@ -1233,7 +1235,7 @@ def main() -> None: help=_deprecated_dev_help, hidden=False, is_eager=True, - callback=_deprecated_dev_cli, + callback=_deprecated_dev_cli_callback, ) @click.option( "-f", @@ -1525,7 +1527,7 @@ def _deprecated_capital_e_callback( help=_deprecated_dev_help, hidden=False, is_eager=True, - callback=_deprecated_dev_cli, + callback=_deprecated_dev_cli_callback, ) @click.option( "-E", @@ -1639,7 +1641,7 @@ def install( help=_deprecated_dev_help, hidden=False, is_eager=True, - callback=_deprecated_dev_cli, + callback=_deprecated_dev_cli_callback, ) @click.option( "-k", @@ -1757,7 +1759,7 @@ def render( help=_deprecated_dev_help, hidden=False, is_eager=True, - callback=_deprecated_dev_cli, + callback=_deprecated_dev_cli_callback, ) @click.option( "-f", From d6969eabd7f7c69f3cb3d89266ff23416e73e566 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sun, 23 Feb 2025 15:11:32 +0100 Subject: [PATCH 09/12] Add test case when dev deps are empty --- tests/test_regression.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/test_regression.py b/tests/test_regression.py index fcf9598f7..532f98465 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -176,6 +176,7 @@ def categories_environment_files(tmp_path: Path) -> List[Path]: dev_deps_and_extras_cli_regression = list( itertools.product( + [False, True], # dev_deps_are_empty [None, True, False], # dev_deps [False, True], # filter_cats [[], ["dev"], ["mm"], ["dev", "mm"]], # extras @@ -184,7 +185,10 @@ def categories_environment_files(tmp_path: Path) -> List[Path]: def make_dev_deps_and_extras_cli_regression_id( - dev_deps: Optional[bool], filter_cats: bool, extras: List[str] + dev_deps_are_empty: bool, + dev_deps: Optional[bool], + filter_cats: bool, + extras: List[str], ) -> str: dev = ( "" @@ -195,16 +199,17 @@ def make_dev_deps_and_extras_cli_regression_id( ) filter = "--filter-categories" if filter_cats else "" extra = "--category=" + ",".join(extras) if extras else "" - nonempty_args = [arg for arg in [dev, filter, extra] if arg] + empty_dev_deps = "empty-dev-deps" if dev_deps_are_empty else "" + nonempty_args = [arg for arg in [dev, filter, extra, empty_dev_deps] if arg] return "_".join(nonempty_args) or "no_args" @pytest.mark.parametrize( - "dev_deps,filter_cats,extras", + "dev_deps_are_empty,dev_deps,filter_cats,extras", dev_deps_and_extras_cli_regression, ids=[ - make_dev_deps_and_extras_cli_regression_id(d, f, e) - for d, f, e in dev_deps_and_extras_cli_regression + make_dev_deps_and_extras_cli_regression_id(dde, d, f, e) + for dde, d, f, e in dev_deps_and_extras_cli_regression ], ) def test_dev_deps_and_extras_cli_regression( @@ -212,6 +217,7 @@ def test_dev_deps_and_extras_cli_regression( categories_environment_files: List[Path], mamba_exe: Path, capsys: "pytest.CaptureFixture[str]", + dev_deps_are_empty: bool, dev_deps: Optional[bool], filter_cats: bool, extras: List[str], @@ -266,16 +272,15 @@ def test_dev_deps_and_extras_cli_regression( "lock", "--conda", str(mamba_exe), - "-p", - "linux-64", - "-k", - "explicit", - "--filename-template", - filename_template, + "--platform=linux-64", + "--kind=explicit", + f"--filename-template={filename_template}", ] - # Add all environment files + # Add environment files for env_file in categories_environment_files: + if dev_deps_are_empty and env_file.name == "environment-dev.yml": + continue args.extend(["-f", str(env_file)]) # Add optional arguments based on the test case @@ -314,7 +319,7 @@ def test_dev_deps_and_extras_cli_regression( assert "tzcode" in content, "Main category dependency should always be present" # Check for dev category dependency - should_have_dev_category = ( + should_have_dev_category = (not dev_deps_are_empty) and ( (dev_deps is None or dev_deps is True) # dev_dependencies is True by default or "dev" in extras ) From aebc6f42b4ce5e9682ee095fdd32b0d59b914a3f Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sun, 23 Feb 2025 15:24:10 +0100 Subject: [PATCH 10/12] Restore previous behavior --- conda_lock/conda_lock.py | 127 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 10 deletions(-) diff --git a/conda_lock/conda_lock.py b/conda_lock/conda_lock.py index bc5be619f..0dd52d939 100644 --- a/conda_lock/conda_lock.py +++ b/conda_lock/conda_lock.py @@ -23,6 +23,7 @@ Dict, Iterator, List, + NamedTuple, Optional, Sequence, Set, @@ -151,6 +152,13 @@ class UnknownLockfileKind(ValueError): pass +class DevDependenciesDeprecationInfo(NamedTuple): + dev_dependencies: Optional[bool] + filter_categories: bool + original_extras: List[str] + override_dev_dependency_deprecation: bool + + def _extract_platform(line: str) -> Optional[str]: search = PLATFORM_PATTERN.search(line) if search: @@ -271,6 +279,7 @@ def make_lock_files( # noqa: C901 with_cuda: Optional[str] = None, strip_auth: bool = False, mapping_url: str, + dev_dependencies_deprecation_info: Optional[DevDependenciesDeprecationInfo] = None, ) -> None: """ Generate a lock file from the src files provided @@ -456,6 +465,7 @@ def make_lock_files( # noqa: C901 filename_template=filename_template, extras=extras, check_input_hash=check_input_hash, + dev_dependencies_deprecation_info=dev_dependencies_deprecation_info, ) @@ -466,6 +476,7 @@ def do_render( extras: Optional[AbstractSet[str]] = None, check_input_hash: bool = False, override_platform: Optional[Sequence[str]] = None, + dev_dependencies_deprecation_info: Optional[DevDependenciesDeprecationInfo] = None, ) -> None: """Render the lock content for each platform in lockfile @@ -505,6 +516,11 @@ def do_render( ) sys.exit(1) + deprecated_dev_dependencies = handle_dev_dependencies_deprecation( + dev_dependencies_deprecation_info, + lockfile, + filename_template, + ) for plat in platforms: for kind in kinds: if filename_template: @@ -515,6 +531,7 @@ def do_render( "timestamp": datetime.datetime.now(datetime.timezone.utc).strftime( "%Y%m%dT%H%M%SZ" ), + "dev-dependencies": str(deprecated_dev_dependencies).lower(), } filename = filename_template.format(**context) @@ -562,6 +579,59 @@ def do_render( ) +def handle_dev_dependencies_deprecation( + dev_dependencies_deprecation_info: Optional[DevDependenciesDeprecationInfo], + lockfile: Lockfile, + filename_template: Optional[str], +) -> bool: + """Handle the deprecation of the dev-dependencies template variable. + + The return value is the boolean value that should be used for the + dev-dependencies template variable. + + It should not be common that dev-dependencies is used as a template variable, + so in these cases we can ignore the deprecation. + + The previous behavior was pretty screwy. It was based solely on the value + of "--dev-dependencies", which defaulted to True, even if there were no + dev dependencies present. + + The desired new behavior is use the presence of dev dependencies in the + lockfile to determine the value of the dev-dependencies template variable. + """ + filename_template_depends_on_dev_dependencies = ( + filename_template is not None and "{dev-dependencies}" in filename_template + ) + + # Whether or not there are dependencies in the "dev" category. + dev_is_a_category = any("dev" in package.categories for package in lockfile.package) + new_dev_dependencies = dev_is_a_category + + # The previous behavior. + deprecated_dev_dependencies = ( + True + if dev_dependencies_deprecation_info is None + or dev_dependencies_deprecation_info.dev_dependencies is None + else dev_dependencies_deprecation_info.dev_dependencies + ) + + # Intervene in the case of an actual discrepancy between the current + # behavior and the deprecated behavior. + if ( + filename_template_depends_on_dev_dependencies + and deprecated_dev_dependencies != new_dev_dependencies + and dev_dependencies_deprecation_info is not None + ): + if deprecated_dev_dependencies and not new_dev_dependencies: + if dev_dependencies_deprecation_info.dev_dependencies is None: + _error_msg = ( + "There are no dev dependencies present, but the 'dev-dependencies' " + "template variable defaulted to 'true'." + ) + ... + return deprecated_dev_dependencies + + def render_lockfile_for_platform( # noqa: C901 *, lockfile: Lockfile, @@ -1060,14 +1130,11 @@ def _deprecated_dev_cli_callback( ctx: click.Context, param: click.Parameter, value: Any ) -> Any: """Raise a deprecation warning and inject `dev` into categories.""" - if value: + if value is not None: warn( "--dev-dependencies/--no-dev-dependencies (lock, render) and --dev/--no-dev (install) " "switches are deprecated. Use `--category dev` instead." ) - ctx.params.setdefault("extras", []) - if "dev" not in ctx.params["extras"]: - ctx.params["extras"].append("dev") return value @@ -1146,6 +1213,7 @@ def run_lock( metadata_yamls: Sequence[pathlib.Path] = (), strip_auth: bool = False, mapping_url: str, + dev_dependencies_deprecation_info: Optional[DevDependenciesDeprecationInfo] = None, ) -> None: if len(environment_files) == 0: environment_files = handle_no_specified_source_files(lockfile_path) @@ -1172,6 +1240,7 @@ def run_lock( metadata_yamls=metadata_yamls, strip_auth=strip_auth, mapping_url=mapping_url, + dev_dependencies_deprecation_info=dev_dependencies_deprecation_info, ) @@ -1226,17 +1295,21 @@ def main() -> None: help="""Override the channels to use when solving the environment. These will replace the channels as listed in the various source files.""", ) @click.option( - "--dev-dependencies", - "--no-dev-dependencies", + "--dev-dependencies/--no-dev-dependencies", "dev_dependencies", - is_flag=True, - flag_value=True, - default=False, + default=None, help=_deprecated_dev_help, hidden=False, is_eager=True, callback=_deprecated_dev_cli_callback, ) +@click.option( + "--override-dev-dependency-deprecation", + is_flag=True, + default=False, + help="Restore the deprecated dev dependency behavior.", + hidden=True, +) @click.option( "-f", "--file", @@ -1372,7 +1445,8 @@ def lock( update: Optional[Sequence[str]] = None, metadata_choices: Sequence[str] = (), metadata_yamls: Sequence[PathLike] = (), - dev_dependencies: bool = False, # DEPRECATED + dev_dependencies: Optional[bool] = None, # DEPRECATED + override_dev_dependency_deprecation: bool = False, ) -> None: """Generate fully reproducible lock files for conda environments. @@ -1417,7 +1491,39 @@ def lock( else: virtual_package_spec = pathlib.Path(virtual_package_spec) + dev_dependencies_deprecation_info = DevDependenciesDeprecationInfo( + dev_dependencies=dev_dependencies, + filter_categories=filter_categories, + original_extras=list(extras), + override_dev_dependency_deprecation=override_dev_dependency_deprecation, + ) + extras_ = set(extras) + if dev_dependencies is None: + extras_.add("dev") + elif dev_dependencies is True: + extras_.add("dev") + warn( + "The --dev-dependencies option is deprecated. Instead, please use " + "--category dev to include the dev category." + ) + elif dev_dependencies is False: + warn( + "The --no-dev-dependencies option is deprecated. Instead, please use " + "'--filter-categories' to exclude the dev category." + ) + filter_categories = True + if "dev" in extras_: + error_msg = ( + "Contradictory options: --no-dev-dependencies and --category=dev or " + "--extras=dev have been specified. Please specify only one of these. " + "To temporarily override this error for now, use " + "--override-dev-dependency-deprecation." + ) + if override_dev_dependency_deprecation: + warn(error_msg) + else: + raise click.UsageError(error_msg) lock_func = partial( run_lock, environment_files=environment_files, @@ -1437,6 +1543,7 @@ def lock( metadata_yamls=[pathlib.Path(path) for path in metadata_yamls], strip_auth=strip_auth, mapping_url=mapping_url, + dev_dependencies_deprecation_info=dev_dependencies_deprecation_info, ) if strip_auth: with tempfile.TemporaryDirectory() as tempdir: From ed2e7b62a6e2352e1094cddf60ac462b9164461b Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sun, 23 Feb 2025 15:32:40 +0100 Subject: [PATCH 11/12] Add override for dev-dependency deprecation --- tests/test_regression.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_regression.py b/tests/test_regression.py index 532f98465..07c8d35a8 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -275,6 +275,7 @@ def test_dev_deps_and_extras_cli_regression( "--platform=linux-64", "--kind=explicit", f"--filename-template={filename_template}", + "--override-dev-dependency-deprecation", ] # Add environment files @@ -298,6 +299,14 @@ def test_dev_deps_and_extras_cli_regression( result = runner.invoke(main, args, catch_exceptions=False) print(result.stdout, file=sys.stdout) print(result.stderr, file=sys.stderr) + # contradictory_options = "dev" in extras and dev_deps is False + # if contradictory_options: + # assert result.exit_code == 2, "Expected exit code 2 for contradictory options" + # assert ( + # "contradictory" in result.stderr.lower() + # ), "Expected error message about contradictory options, got: " + result.stderr + # return + # else: assert result.exit_code == 0 # Verify exactly one output file was generated From 73c3cfff72f21da45ec69f7c4b4ee990f0aebc91 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sat, 22 Mar 2025 20:16:16 +0100 Subject: [PATCH 12/12] Push local changes --- conda_lock/conda_lock.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/conda_lock/conda_lock.py b/conda_lock/conda_lock.py index 0dd52d939..f93f8ed4f 100644 --- a/conda_lock/conda_lock.py +++ b/conda_lock/conda_lock.py @@ -622,13 +622,23 @@ def handle_dev_dependencies_deprecation( and deprecated_dev_dependencies != new_dev_dependencies and dev_dependencies_deprecation_info is not None ): - if deprecated_dev_dependencies and not new_dev_dependencies: - if dev_dependencies_deprecation_info.dev_dependencies is None: + if new_dev_dependencies and not deprecated_dev_dependencies: + if dev_dependencies_deprecation_info.dev_dependencies is False: _error_msg = ( - "There are no dev dependencies present, but the 'dev-dependencies' " - "template variable defaulted to 'true'." + "There are dev dependencies present, despite having " + "specified --no-dev-dependencies. Consequently, the " + "{dev-dependencies} template variable has been set to 'false'. " + "The value of {dev-dependencies} will change to 'true' in a " + "future version of conda-lock." ) - ... + else: + _error_msg = ( + "There is a discrepancy between the current behavior and the " + "deprecated behavior. The current case is unexpected. Please " + "report this as a bug to ..." + ) + else: + raise click.UsageError("x") return deprecated_dev_dependencies