diff --git a/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirementImpl.kt b/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirementImpl.kt index ec289b9e85ff..3ebe66dc8ce8 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirementImpl.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirementImpl.kt @@ -45,12 +45,12 @@ class PyRequirementImpl( return when (other) { is String -> name == normalizePackageName(other) - is PyRequirementImpl -> name == other.name // TODO: should we match specs & options ? + is PyRequirementImpl -> name == other.name && versionSpecs == other.versionSpecs else -> false } } - override fun hashCode(): Int = name.hashCode() + override fun hashCode(): Int = 31 * name.hashCode() + versionSpecs.hashCode() override fun toString(): String { return presentableText diff --git a/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirementParser.java b/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirementParser.java index 334ade26e057..693a8e9f0e22 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirementParser.java +++ b/python/python-psi-impl/src/com/jetbrains/python/packaging/PyRequirementParser.java @@ -30,9 +30,6 @@ import static com.jetbrains.python.packaging.parser.RequirementsParserHelper.VCS * @see PEP-440 * @see PyRequirement * @see PyPackageVersionNormalizer - * @see PyPackageManager#parseRequirement(String) - * @see PyPackageManager#parseRequirements(String) - * @see PyPackageManager#parseRequirements(VirtualFile) */ public final class PyRequirementParser { @@ -148,8 +145,8 @@ public final class PyRequirementParser { private static final @NotNull String REQUIREMENT_VERSION_SPEC_REGEXP = "(<=?|!=|===?|>=?|~=)" + LINE_WS_REGEXP + "*[\\.\\*\\+!\\w-]+"; private static final @NotNull String REQUIREMENT_VERSIONS_SPECS_REGEXP = - "(?<" + REQUIREMENT_VERSIONS_SPECS_GROUP + ">" + REQUIREMENT_VERSION_SPEC_REGEXP + - "(" + LINE_WS_REGEXP + "*," + LINE_WS_REGEXP + "*" + REQUIREMENT_VERSION_SPEC_REGEXP + ")*)?"; + "(?<" + REQUIREMENT_VERSIONS_SPECS_GROUP + ">\\(?" + REQUIREMENT_VERSION_SPEC_REGEXP + + "(" + LINE_WS_REGEXP + "*," + LINE_WS_REGEXP + "*" + REQUIREMENT_VERSION_SPEC_REGEXP + ")*\\)?)?"; private static final @NotNull String REQUIREMENT_OPTIONS_GROUP = "options"; @@ -371,6 +368,11 @@ public final class PyRequirementParser { private static @NotNull List parseVersionSpecs(@Nullable String versionSpecs) { if (versionSpecs == null) return Collections.emptyList(); + versionSpecs = versionSpecs.trim(); + if (versionSpecs.startsWith("(") && versionSpecs.endsWith(")")) { + versionSpecs = versionSpecs.substring(1, versionSpecs.length() - 1); + } + return StreamSupport .stream(StringUtil.tokenize(versionSpecs, ",").spliterator(), false) .map(String::trim) diff --git a/python/testData/packaging/PyPackageUtil/SetupPyDependencyLinksReading/setup.py b/python/testData/packaging/PyPackageUtil/SetupPyDependencyLinksReading/setup.py index f2e6f8edb8e3..693cca3f08a6 100644 --- a/python/testData/packaging/PyPackageUtil/SetupPyDependencyLinksReading/setup.py +++ b/python/testData/packaging/PyPackageUtil/SetupPyDependencyLinksReading/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup(name='foo', version=0.1, install_requires = ["sqlalchemy >=1.0.12, <1.1", - "mysql-connector-python >=2.1.3, <2.2",], + "mysql-connector-python ==2.1.3",], dependency_links = [ "git+https://github.com/mysql/mysql-connector-python.git@2.1.3#egg=mysql-connector-python-2.1.3", ]) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java b/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java index b6b3099df2c1..5c658dab5b06 100644 --- a/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java +++ b/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java @@ -1969,6 +1969,18 @@ public class PyRequirementTest extends PyTestCase { assertEquals(pyRequirement("no_limit_nester", EQ, "1.0+local.version.10"), fromLine("no_limit_nester==1.0+local.version.10")); } + public void testRequirementVersionWithBraces() { + assertEquals(pyRequirement("Orange-Bioinformatics", EQ, "2.5a20"), fromLine("Orange-Bioinformatics (==2.5a20)")); + assertEquals(pyRequirement("MOCPy", EQ, "0.1.0.dev0"), fromLine("MOCPy (==0.1.0.dev0)")); + assertEquals(pyRequirement("score.webassets", EQ, "0.2.3"), fromLine("score.webassets (==0.2.3)")); + assertEquals(pyRequirement("pip_helpers", EQ, "0.5.post6"), fromLine("pip_helpers (==0.5.post6)")); + assertEquals(pyRequirement("Django", EQ, "1.9rc1"), fromLine("Django (==1.9rc1)")); + assertEquals(pyRequirement("django", EQ, "1!1"), fromLine("django (==1!1)")); + assertEquals(pyRequirement("pinax-utils", EQ, "1.0b1.dev3"), fromLine("pinax-utils (==1.0b1.dev3)")); + assertEquals(pyRequirement("Flask-Celery-py3", EQ, "0.1.*"), fromLine("Flask-Celery-py3 (==0.1.*)")); + assertEquals(pyRequirement("no_limit_nester", EQ, "1.0+local.version.10"), fromLine("no_limit_nester (==1.0+local.version.10)")); + } + // https://www.python.org/dev/peps/pep-0440/#normalization public void testRequirementAlternatePreReleaseVersion() { doRequirementVersionNormalizationTest("1.9rc1", "1.9RC1"); diff --git a/python/testSrc/com/jetbrains/python/packaging/conda/CondaEnvironmentTest.kt b/python/testSrc/com/jetbrains/python/packaging/conda/CondaEnvironmentTest.kt index 6f48d1ae8d65..5406b6648b27 100644 --- a/python/testSrc/com/jetbrains/python/packaging/conda/CondaEnvironmentTest.kt +++ b/python/testSrc/com/jetbrains/python/packaging/conda/CondaEnvironmentTest.kt @@ -31,7 +31,7 @@ class CondaEnvironmentTest { // Exact version specifications PyRequirementParser.fromLine("scipy==1.9.3")!!, - PyRequirementParser.fromLine("requests==2.28.1")!!, + PyRequirementParser.fromLine("requests~=2.28.1")!!, PyRequirementParser.fromLine("flask==2.2.2")!!, // Version ranges with operators