Repository: conan-io/conan
Pull request: Improve error messages for invalid version ranges operators
Changelog: Fix: Improve the definition of version ranges UX with better error message for invalid ``==, ~=, ^=`` operators.
Docs: Omit

Close https://github.com/conan-io/conan/issues/16066

Problem statement:
[bug] Exact version range `[==...]` not working anymore with conan2
### Describe the bug

Hello,
I'm currently in the process of upgrading recipes from conan1 to conan 2 and I found out that `package/[==1.0.0]` is no longer working with conan2 (but I couldn't find anything about that in the doc) and conan doesn't throw an error like for `[~=1.0]` which is now explicitly invalid.

```
ERROR: Package 'package_name/[==1.0.0]@pypi/stable' not resolved: Version range '==1.0.0' from requirement 'package_name/[==1.0.0]@pypi/stable' required by ... could not be resolved.
```

It would be nice to either throw a proper error message ("this version range is no longer supported, please upgrade to ..." and update the doc) or fix the bug and re-add support for it.



### How to reproduce it

`conan install --requires "any_pkg/[==1.0.0]@user/channel"` vs `conan install --requires "any_pkg/1.0.0@user/channel"`

Hints from issue discussion:
Hi @araffin 

Thanks for pointing this out.

The truth is that Conan 1 never intended to support ``[==version]`` syntax, for example the docs in https://docs.conan.io/1/versioning/version_ranges.html doesn't describe this syntax. Furthermore the https://github.com/podhmo/python-node-semver doesn't explicitly mention it either (it is true that docs are almost non-existing)

So it seems the python-node-semver implemented it and it "slipped" as a feature in Conan 1 we were not very aware, because the recommended approach for pinned dependencies in Conan both 1 and 2 is ``pkg/version`` without the ``[version]`` brackets.

As we implemented our own version ranges in Conan 2, the ``==`` was not managed at all, and this is what you are seeing.

I'll check if possible to throw a better error, or at least document it.
@araffin Is there any reason to use `package/[==1.0.0]` instead of plain `package/1.0.0`? It also has the potential unintended side effect of ignoring pre-release versions (e.g. `1.0.0-rc1`).
I was thinking of allowing the ``==`` syntax in Conan 2, but I cannot find any reference online about this syntax, I have checked some resources like the closest to an official thing (https://docs.npmjs.com/cli/v6/using-npm/semver) to other languages docs, but no success.
Thanks for the swift answer, I guess a better error message would be helpful then.

> Is there any reason to use package/[==1.0.0] instead of plain package/1.0.0? 

To give a bit more context, I wrote a script to automatically convert python packages (from PyPi) to conan ones, and this was the most natural way to do the translation (for pinned version. For other version ranges, it is more hacky because pip and conan don't agree).

Side note: `[1.0.0]` do work with conan2.
> conan doesn't throw an error like for `[~=1.0]` which is now explicitly invalid.

I would argue, that the error message for `[~=1.0]` could also be improved in conan2: 

`Cannot bump '=2- version index 0, not an int` is an explicit error, but not explicitly pointing towards the actual issue.

So if introducing a message for `[==1.0]` maybe you would also like to consider a new message for `[~=1.0]` as well.

> To give a bit more context, I wrote a script to automatically convert python packages (from PyPi) to conan ones, and this was the most natural way to do the translation (for pinned version. For other version ranges, it is more hacky because pip and conan don't agree).

That explains it. The way we understood the Python/pip requirements is that ``pkg==version`` means a pinned dependency, i.e., that is not a version range, but a fixed dependency, while other expressions like ``>=`` define a version range. For context, we also treat it differently in Conan because initially Conan didn't have version ranges, it only implemented fixed/pinned dependencies. It was later that the version ranges were introduced with the ``[]`` syntax to avoid potential breakage, and we distinguish between pinned deps without brackets and version ranges with brackets. It is true that some equality expressions are admitted in version ranges, this is because of the ``[>=1 <2  || 3.1]`` syntax, very uncommon and not recommended for most cases, but supported.

I am submitting a PR to raise a better error message.

Commit message: wip

Patch to review:
diff --git a/conans/model/version_range.py b/conans/model/version_range.py
index 97778f68cf6..83aeb7c745e 100644
--- a/conans/model/version_range.py
+++ b/conans/model/version_range.py
@@ -88,6 +88,8 @@ def _parse_expression(expression):
             if expression[1] == "=":
                 operator += "="
                 index = 2
+        elif expression[1] == "=":
+            raise ConanException(f"Invalid version range operator '{operator}=' in {expression}")
         version = expression[index:]
         if version == "":
             raise ConanException(f'Error parsing version range "{expression}"')
@@ -111,7 +113,7 @@ def first_non_zero(main):
         else:
             return [_Condition(operator, Version(version))]
 
-    def _valid(self, version, conf_resolve_prepreleases):
+    def valid(self, version, conf_resolve_prepreleases):
         if version.pre:
             # Follow the expression desires only if core.version_ranges:resolve_prereleases is None,
             # else force to the conf's value
@@ -180,7 +182,7 @@ def contains(self, version: Version, resolve_prerelease: Optional[bool]):
         """
         assert isinstance(version, Version), type(version)
         for condition_set in self.condition_sets:
-            if condition_set._valid(version, resolve_prerelease):
+            if condition_set.valid(version, resolve_prerelease):
                 return True
         return False
 
diff --git a/conans/test/unittests/model/version/test_version_range.py b/conans/test/unittests/model/version/test_version_range.py
index 263caf65f82..256d4420eab 100644
--- a/conans/test/unittests/model/version/test_version_range.py
+++ b/conans/test/unittests/model/version/test_version_range.py
@@ -5,6 +5,7 @@
 from conans.model.version_range import VersionRange
 
 values = [
+    ['=1.0.0',  [[['=', '1.0.0']]],                   ["1.0.0"],          ["0.1"]],
     ['>1.0.0',  [[['>', '1.0.0']]],                   ["1.0.1"],          ["0.1"]],
     ['<2.0',    [[['<', '2.0-']]],                     ["1.0.1"],          ["2.1"]],
     ['>1 <2.0', [[['>', '1'], ['<', '2.0-']]],         ["1.5.1"],          ["0.1", "2.1"]],
@@ -109,7 +110,8 @@ def test_range_prereleases_conf(version_range, resolve_prereleases, versions_in,
 
 @pytest.mark.parametrize("version_range", [
     ">= 1.0",  # https://github.com/conan-io/conan/issues/12692
-    ">=0.0.1 < 1.0"  # https://github.com/conan-io/conan/issues/14612
+    ">=0.0.1 < 1.0",  # https://github.com/conan-io/conan/issues/14612
+    "==1.0",  "~=1.0",  "^=1.0", "v=1.0"  # https://github.com/conan-io/conan/issues/16066
 ])
 def test_wrong_range_syntax(version_range):
     with pytest.raises(ConanException):


Code review comments:
diff_hunk: @@ -88,6 +88,8 @@ def _parse_expression(expression):
             if expression[1] == "=":
                 operator += "="
                 index = 2
+        elif expression[1] == "=":
+            raise ConanException(f"Invalid version range operator '{operator}=' in {expression}")
path: conans/model/version_range.py
text: @user1:
```suggestion
            raise ConanException(f"Invalid version range operator '{operator}=' in {expression}, you should probably use {operator} instead.")
```
Maybe give a hint to the user on what is a correct range (the proposed error messages doesn't work in all cases, for instance for the `==` one)

@user1:
a better error message would probably be to distinguish between `~=`, `^=`, `==` and the rest.