diff --git a/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java b/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java index 07683cc619e1..9dc6fae00945 100644 --- a/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java +++ b/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java @@ -24,6 +24,7 @@ import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.webcore.packaging.PackageVersionComparator; import com.jetbrains.python.packaging.requirement.PyRequirementRelation; +import com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer; import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -44,10 +45,7 @@ public class PyRequirement { private static final String LINE_WS_REGEXP = "[ \t]"; @NotNull - private static final String EDITABLE_GROUP = "editable"; - - @NotNull - private static final String EDITABLE_REGEXP = "((?<" + EDITABLE_GROUP + ">-e|--editable)" + LINE_WS_REGEXP + "+)?"; + private static final String EDITABLE_REGEXP = "((-e|--editable)" + LINE_WS_REGEXP + "+)?"; @NotNull private static final String USER_AT_REGEXP = "[\\w-]+@"; @@ -274,7 +272,7 @@ public class PyRequirement { final Matcher matcher = GITHUB_ARCHIVE_URL.matcher(line); if (matcher.matches()) { - return new PyRequirement(matcher.group(NAME_GROUP), null, line, false); + return new PyRequirement(matcher.group(NAME_GROUP), line, Collections.emptyList()); } return null; @@ -285,9 +283,7 @@ public class PyRequirement { final Matcher matcher = ARCHIVE_URL.matcher(line); if (matcher.matches()) { - final Pair nameAndVersion = parseNameAndVersion(matcher.group(NAME_GROUP)); - - return new PyRequirement(nameAndVersion.getFirst(), nameAndVersion.getSecond(), line, false); + return createVcsOrArchiveRequirement(line, parseNameAndVersionFromVcsOrArchive(matcher.group(NAME_GROUP))); } return null; @@ -351,7 +347,7 @@ public class PyRequirement { } @NotNull - private static Pair parseNameAndVersion(@NotNull String name) { + private static Pair parseNameAndVersionFromVcsOrArchive(@NotNull String name) { boolean isName = true; final List nameParts = new ArrayList(); final List versionParts = new ArrayList(); @@ -371,19 +367,36 @@ public class PyRequirement { } } - return Pair.create(normalizeNameParts(nameParts), normalizeVersionParts(versionParts)); + return Pair.create(normalizeVcsOrArchiveNameParts(nameParts), normalizeVcsOrArchiveVersionParts(versionParts)); + } + + @NotNull + private static PyRequirement createVcsOrArchiveRequirement(@NotNull String line, @NotNull Pair nameAndVersion) { + final String name = nameAndVersion.getFirst(); + final String version = nameAndVersion.getSecond(); + + if (version == null) { + return new PyRequirement(name, line, Collections.emptyList()); + } + + final String normalizedVersion = PyRequirementVersionNormalizer.normalize(version); + final PyRequirementVersionSpec versionSpec = normalizedVersion == null ? + new PyRequirementVersionSpec(PyRequirementRelation.STR_EQ, version) : + new PyRequirementVersionSpec(PyRequirementRelation.EQ, normalizedVersion); + + return new PyRequirement(name, line, Collections.singletonList(versionSpec)); } @NotNull private static PyRequirement createVcsRequirement(@NotNull String line, @NotNull Matcher matcher) { - final boolean editable = matcher.group(EDITABLE_GROUP) != null; final String path = matcher.group(PATH_GROUP); final String egg = matcher.group(EGG_GROUP); final String project = extractProject(dropTrunk(dropRevision(path))); - final Pair nameAndVersion = parseNameAndVersion(egg == null ? StringUtil.trimEnd(project, ".git") : egg); + final Pair nameAndVersion = + parseNameAndVersionFromVcsOrArchive(egg == null ? StringUtil.trimEnd(project, ".git") : egg); - return new PyRequirement(nameAndVersion.getFirst(), nameAndVersion.getSecond(), line, editable); + return createVcsOrArchiveRequirement(line, nameAndVersion); } @NotNull @@ -414,12 +427,12 @@ public class PyRequirement { } @NotNull - private static String normalizeNameParts(@NotNull List nameParts) { + private static String normalizeVcsOrArchiveNameParts(@NotNull List nameParts) { return normalizeName(StringUtil.join(nameParts, "-")); } @Nullable - private static String normalizeVersionParts(@NotNull List versionParts) { + private static String normalizeVcsOrArchiveVersionParts(@NotNull List versionParts) { return versionParts.isEmpty() ? null : normalizeVersion(StringUtil.join(versionParts, "-")); } @@ -498,8 +511,19 @@ public class PyRequirement { if (relation != null) { final int versionIndex = findFirstNotWhiteSpace(versionSpec, relation.toString().length()); + final String version = versionSpec.substring(versionIndex); - return new PyRequirementVersionSpec(relation, versionSpec.substring(versionIndex)); + if (relation == PyRequirementRelation.STR_EQ) { + return new PyRequirementVersionSpec(relation, version); + } + + final String normalizedVersion = PyRequirementVersionNormalizer.normalize(version); + + if (normalizedVersion == null) { + return new PyRequirementVersionSpec(PyRequirementRelation.STR_EQ, version); + } else { + return new PyRequirementVersionSpec(relation, normalizedVersion); + } } return null; diff --git a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionNormalizer.java b/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionNormalizer.java new file mode 100644 index 000000000000..3c5599ff4622 --- /dev/null +++ b/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionNormalizer.java @@ -0,0 +1,197 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.packaging.requirement; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class PyRequirementVersionNormalizer { + + @NotNull + private static final String EPOCH_GROUP = "epoch"; + + @NotNull + private static final String RELEASE_GROUP = "release"; + + @NotNull + private static final String PRE_RELEASE_TYPE_GROUP = "pretype"; + + @NotNull + private static final String PRE_RELEASE_NUMBER_GROUP = "prenumber"; + + @NotNull + private static final String POST_RELEASE_TYPE_GROUP = "posttype"; + + @NotNull + private static final String POST_RELEASE_NUMBER_GROUP = "postnumber"; + + @NotNull + private static final String IMPLICIT_POST_RELEASE_NUMBER_GROUP = "implicitpostnumber"; + + @NotNull + private static final String DEV_RELEASE_TYPE_GROUP = "devtype"; + + @NotNull + private static final String DEV_RELEASE_NUMBER_GROUP = "devnumber"; + + @NotNull + private static final String LOCAL_VERSION_GROUP = "local"; + + @NotNull + private static final String SEP_REGEXP = "(\\.|-|_)?"; + + @NotNull + private static final String EPOCH_REGEXP = "(?<" + EPOCH_GROUP + ">\\d+!)?"; + + @NotNull + private static final String RELEASE_REGEXP = "(?<" + RELEASE_GROUP + ">(\\d+(\\.\\d+)*)|(\\d+\\.(\\d+\\.)*\\*))"; + + @NotNull + private static final String PRE_RELEASE_REGEXP = + "(" + + SEP_REGEXP + + "(?<" + PRE_RELEASE_TYPE_GROUP + ">a|alpha|b|beta|rc|c|pre|preview)" + + "(" + SEP_REGEXP + "(?<" + PRE_RELEASE_NUMBER_GROUP + ">\\d+))?" + + ")?"; + + @NotNull + private static final String POST_RELEASE_REGEXP = + "(" + + "(" + SEP_REGEXP + "(?<" + POST_RELEASE_TYPE_GROUP + ">post|rev|r)(" + SEP_REGEXP + "(?<" + POST_RELEASE_NUMBER_GROUP + ">\\d+))?)" + + "|" + + "(-(?<" + IMPLICIT_POST_RELEASE_NUMBER_GROUP + ">\\d+))" + + ")?"; + + @NotNull + private static final String DEV_RELEASE_REGEXP = + "(" + + SEP_REGEXP + "(?<" + DEV_RELEASE_TYPE_GROUP + ">dev)(?<" + DEV_RELEASE_NUMBER_GROUP + ">\\d+)?" + + ")?"; + + @NotNull + private static final String LOCAL_VERSION_REGEXP = "(?<" + LOCAL_VERSION_GROUP + ">\\+[a-z0-9]([a-z0-9\\._-]*[a-z0-9])?)?"; + + @NotNull + private static final Pattern VERSION = Pattern.compile( + "^" + + "v?" + + EPOCH_REGEXP + + RELEASE_REGEXP + + PRE_RELEASE_REGEXP + + POST_RELEASE_REGEXP + + DEV_RELEASE_REGEXP + + LOCAL_VERSION_REGEXP + + "$", + Pattern.CASE_INSENSITIVE); + + @Nullable + public static String normalize(@NotNull String version) { + final Matcher matcher = VERSION.matcher(version); + if (matcher.matches()) { + final StringBuilder sb = new StringBuilder(); + + final String epoch = matcher.group(EPOCH_GROUP); + if (epoch != null) { + final String normalizedEpoch = normalizeNumber(epoch.substring(0, epoch.length() - 1)); + sb + .append(normalizedEpoch) + .append('!'); + } + + for (String releasePart : StringUtil.tokenize(matcher.group(RELEASE_GROUP), ".")) { + sb + .append(releasePart.equals("*") ? "*" : normalizeNumber(releasePart)) + .append('.'); + } + + if (sb.charAt(sb.length() - 1) == '.') { + sb.setLength(sb.length() - 1); + } + + final String preReleaseType = matcher.group(PRE_RELEASE_TYPE_GROUP); + if (preReleaseType != null) { + final String preReleaseNumber = matcher.group(PRE_RELEASE_NUMBER_GROUP); + final String normalizedPreReleaseNumber = preReleaseNumber == null ? "0" : normalizeNumber(preReleaseNumber); + + sb + .append(normalizePreReleaseType(preReleaseType)) + .append(normalizedPreReleaseNumber); + } + + final String postReleaseType = matcher.group(POST_RELEASE_TYPE_GROUP); + if (postReleaseType != null) { + final String postReleaseNumber = matcher.group(POST_RELEASE_NUMBER_GROUP); + final String normalizedPostReleaseNumber = postReleaseNumber == null ? "0" : normalizeNumber(postReleaseNumber); + + sb + .append(".post") + .append(normalizeNumber(normalizedPostReleaseNumber)); + } + + final String implicitPostReleaseNumber = matcher.group(IMPLICIT_POST_RELEASE_NUMBER_GROUP); + if (implicitPostReleaseNumber != null) { + sb + .append(".post") + .append(normalizeNumber(implicitPostReleaseNumber)); + } + + if (matcher.group(DEV_RELEASE_TYPE_GROUP) != null) { + final String devReleaseNumber = matcher.group(DEV_RELEASE_NUMBER_GROUP); + final String normalizedDevReleaseNumber = devReleaseNumber == null ? "0" : normalizeNumber(devReleaseNumber); + + sb + .append(".dev") + .append(normalizedDevReleaseNumber); + } + + final String localVersion = matcher.group(LOCAL_VERSION_GROUP); + if (localVersion != null) { + sb.append(normalizeLocalVersion(localVersion)); + } + + return sb.toString(); + } + + return null; + } + + @NotNull + private static String normalizeNumber(@NotNull String number) { + return Integer.valueOf(number).toString(); + } + + @NotNull + private static String normalizePreReleaseType(@NotNull String preReleaseType) { + if (preReleaseType.equalsIgnoreCase("a") || preReleaseType.equalsIgnoreCase("alpha")) { + return "a"; + } + else if (preReleaseType.equalsIgnoreCase("b") || preReleaseType.equalsIgnoreCase("beta")) { + return "b"; + } + else { + return "rc"; + } + } + + @NotNull + private static String normalizeLocalVersion(@NotNull String localVersion) { + return localVersion.replaceAll("[-_]", "."); + } +}