From 2a94bba7919e15fc4df971708c29e44aab3efbc7 Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Thu, 10 May 2018 20:07:14 +0300 Subject: [PATCH] Apply deprecation notes in packaging Make PyRequirementVersionSpec an interface Make PyRequirement an interface Add documentation to packaging classes that could be useful for other developers --- .../ipnb/run/IpnbRunConfiguration.java | 24 +- .../python/packaging/PyPackageManager.java | 31 + .../python/packaging/PyRequirement.java | 860 +----------------- .../requirement/PyRequirementRelation.java | 11 +- .../requirement/PyRequirementVersion.kt | 39 - .../requirement/PyRequirementVersionSpec.java | 166 +--- .../PyIntegratedToolsConfigurable.java | 4 +- .../PyPackageRequirementsInspection.java | 4 +- .../PyUnresolvedReferencesInspection.java | 3 +- .../packaging/PyCondaPackageManagerImpl.java | 6 +- .../packaging/PyPackageManagerImpl.java | 20 +- .../python/packaging/PyPackageUtil.java | 82 +- .../python/packaging/PyPackageVersion.kt | 32 +- .../packaging/PyPackageVersionComparator.kt | 25 +- .../PyPackageVersionNormalizer.java} | 26 +- .../python/packaging/PyPackagingSettings.java | 4 +- .../python/packaging/PyRequirementImpl.kt | 30 + .../python/packaging/PyRequirementParser.java | 663 ++++++++++++++ .../python/packaging/PyRequirements.kt | 126 +++ .../ui/PyChooseRequirementsDialog.java | 2 +- .../ui/PyPackageManagementService.java | 11 +- .../jetbrains/env/python/PyPackagingTest.java | 14 +- .../python/packaging/PyPackageTest.java | 2 +- .../python/packaging/PyPackageUtilTest.java | 6 +- .../PyPackageVersionComparatorTest.kt | 78 +- .../python/packaging/PyRequirementTest.java | 402 ++++---- 26 files changed, 1245 insertions(+), 1426 deletions(-) delete mode 100644 python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersion.kt rename python/{openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionNormalizer.java => src/com/jetbrains/python/packaging/PyPackageVersionNormalizer.java} (86%) create mode 100644 python/src/com/jetbrains/python/packaging/PyRequirementImpl.kt create mode 100644 python/src/com/jetbrains/python/packaging/PyRequirementParser.java create mode 100644 python/src/com/jetbrains/python/packaging/PyRequirements.kt diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/run/IpnbRunConfiguration.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/run/IpnbRunConfiguration.java index 7ca5cf18d454..8c7e4aafded2 100644 --- a/python/ipnb/src/org/jetbrains/plugins/ipnb/run/IpnbRunConfiguration.java +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/run/IpnbRunConfiguration.java @@ -1,6 +1,5 @@ package org.jetbrains.plugins.ipnb.run; -import com.google.common.collect.Lists; import com.intellij.execution.ExecutionException; import com.intellij.execution.Executor; import com.intellij.execution.configurations.ConfigurationFactory; @@ -23,7 +22,7 @@ import com.intellij.remote.RemoteSdkCredentialsHolder; import com.jetbrains.python.packaging.PyPackage; import com.jetbrains.python.packaging.PyPackageManager; import com.jetbrains.python.packaging.PyPackageUtil; -import com.jetbrains.python.packaging.PyRequirement; +import com.jetbrains.python.packaging.requirement.PyRequirementRelation; import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.run.AbstractPythonRunConfiguration; import com.jetbrains.python.run.DebugAwareConfiguration; @@ -32,8 +31,12 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static com.jetbrains.python.packaging.PyRequirementsKt.pyRequirement; + public class IpnbRunConfiguration extends AbstractPythonRunConfiguration implements DebugAwareConfiguration { @NonNls private static final String ATTR_ADDITIONAL_OPTIONS = "additionalOptions"; @@ -95,9 +98,12 @@ public class IpnbRunConfiguration extends AbstractPythonRunConfiguration packages = PyPackageManager.getInstance(sdk).getPackages(); + final PyPackageManager packageManager = PyPackageManager.getInstance(sdk); + final List packages = packageManager.getPackages(); + final PyPackage ipythonPackage = packages != null ? PyPackageUtil.findPackage(packages, "ipython") : null; final PyPackage jupyterPackage = packages != null ? PyPackageUtil.findPackage(packages, "jupyter") : null; + if (ipythonPackage == null && jupyterPackage == null) { throw new RuntimeConfigurationError("Install Jupyter Notebook to the interpreter of the current project.", () -> ProgressManager.getInstance().run(new Task.Backgroundable(getProject(), @@ -109,12 +115,16 @@ public class IpnbRunConfiguration extends AbstractPythonRunConfiguration getRequirements(@NotNull Module module); + /** + * @param line requirement description + * @return parsed requirement or null if given description could not be parsed. + * @see pip install documentation + * @see PEP-508 + * @see PEP-440 + */ + @Nullable + public abstract PyRequirement parseRequirement(@NotNull String line); + + /** + * @param text requirements descriptions + * @return parsed requirements. + * Note: the returned list does not contain null or repetitive values, descriptions that could not be parsed are skipped. + * @see pip install documentation + * @see PEP-508 + * @see PEP-440 + */ @NotNull public abstract List parseRequirements(@NotNull String text); + /** + * @param file file containing requirements descriptions. + * Used as a foothold to resolve recursive requirements specified through -r or --requirement flags. + * @return parsed requirements. + * Note: the returned list does not contain null or repetitive values, descriptions that could not be parsed are skipped. + * @see pip install documentation + * @see PEP-508 + * @see PEP-440 + */ + @NotNull + public abstract List parseRequirements(@NotNull VirtualFile file); + @NotNull public abstract Set getDependents(@NotNull PyPackage pkg) throws ExecutionException; diff --git a/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java b/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java index 9494b35159f6..01cedcb96959 100644 --- a/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java +++ b/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java @@ -13,865 +13,59 @@ // limitations under the License. package com.jetbrains.python.packaging; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.util.containers.ContainerUtil; -import com.jetbrains.python.packaging.requirement.PyRequirementRelation; -import com.jetbrains.python.packaging.requirement.PyRequirementVersion; -import com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer; import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec; -import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; +import java.util.Collection; +import java.util.List; + /** - * @apiNote This class will be converted to interface in 2018.2. + * @see pip install documentation + * @see PEP-508 + * @see PEP-440 + * @see PyPackageManager#parseRequirement(String) + * @see PyPackageManager#parseRequirements(String) + * @see PyPackageManager#parseRequirements(VirtualFile) */ -public class PyRequirement { - - // common regular expressions +public interface PyRequirement { @NotNull - private static final String LINE_WS_REGEXP = "[ \t]"; + String getName(); @NotNull - private static final String COMMENT_GROUP = "comment"; - - @NotNull - private static final String COMMENT_REGEXP = "(?<" + COMMENT_GROUP + ">" + LINE_WS_REGEXP + "+#.*)?"; - - @NotNull - private static final String NAME_GROUP = "name"; - - // PEP-508 - // https://www.python.org/dev/peps/pep-0508/ - - @NotNull - private static final String IDENTIFIER_REGEXP = "[A-Za-z0-9]([-_\\.]?[A-Za-z0-9])*"; - - @NotNull - private static final String NAME_REGEXP = "(?<" + NAME_GROUP + ">" + IDENTIFIER_REGEXP + ")"; - - @NotNull - private static final String EXTRAS_REGEXP = - "\\[" + IDENTIFIER_REGEXP + "(" + LINE_WS_REGEXP + "*," + LINE_WS_REGEXP + "*" + IDENTIFIER_REGEXP + ")*" + "\\]"; - - // archive-related regular expressions - - @NotNull - private static final Pattern GITHUB_ARCHIVE_URL = - Pattern.compile("https?://github\\.com/[^/\\s]+/(?<" + NAME_GROUP + ">[^/\\s]+)/archive/\\S+" + COMMENT_REGEXP); - - @NotNull - private static final Pattern GITLAB_ARCHIVE_URL = - Pattern.compile("https?://gitlab\\.com/[^/\\s]+/(?<" + NAME_GROUP + ">[^/\\s]+)/repository/\\S+" + COMMENT_REGEXP); - - @NotNull - private static final Pattern ARCHIVE_URL = - Pattern.compile("https?://\\S+/" + - "(?<" + NAME_GROUP + ">\\S+)" + - "(\\.tar\\.gz|\\.zip)(#(sha1|sha224|sha256|sha384|sha512|md5)=\\w+)?" + COMMENT_REGEXP); - - // vcs-related regular expressions - // don't forget to update calculateVcsInstallOptions(Matcher) after this section changing - - @NotNull - private static final String VCS_EDITABLE_GROUP = "editable"; - - @NotNull - private static final String VCS_EDITABLE_REGEXP = "((?<" + VCS_EDITABLE_GROUP + ">-e|--editable)" + LINE_WS_REGEXP + "+)?"; - - @NotNull - private static final String VCS_SRC_BEFORE_GROUP = "srcb"; - - @NotNull - private static final String VCS_SRC_AFTER_GROUP = "srca"; - - @NotNull - private static final String VCS_SRC_BEFORE_REGEXP = - "(?<" + VCS_SRC_BEFORE_GROUP + ">--src" + LINE_WS_REGEXP + "+\\S+" + LINE_WS_REGEXP + "+)?"; - - @NotNull - private static final String VCS_SRC_AFTER_REGEXP = - "(?<" + VCS_SRC_AFTER_GROUP + ">" + LINE_WS_REGEXP + "+--src" + LINE_WS_REGEXP + "+\\S+)?"; - - @NotNull - private static final String PATH_IN_VCS_GROUP = "path"; - - @NotNull - private static final String PATH_IN_VCS_REGEXP = "(?<" + PATH_IN_VCS_GROUP + ">[^@#\\s]+)"; - - @NotNull - private static final String VCS_REVISION_REGEXP = "(@[^#\\s]+)?"; - - @NotNull - private static final String VCS_EGG_BEFORE_SUBDIR_GROUP = "eggb"; - - @NotNull - private static final String VCS_EGG_AFTER_SUBDIR_GROUP = "egga"; - - @NotNull - private static final String VCS_EXTRAS_BEFORE_SUBDIR_GROUP = "extrasb"; - - @NotNull - private static final String VCS_EXTRAS_AFTER_SUBDIR_GROUP = "extrasa"; - - @NotNull - private static final String VCS_PARAMS_REGEXP = - "(" + - "(" + - "#egg=(?<" + VCS_EGG_BEFORE_SUBDIR_GROUP + ">[^&\\s\\[\\]]+)(?<" + VCS_EXTRAS_BEFORE_SUBDIR_GROUP + ">" + EXTRAS_REGEXP + ")?" + - "(&subdirectory=\\S+)?" + - ")" + - "|" + - "(" + - "#subdirectory=[^&\\s]+" + - "&egg=(?<" + VCS_EGG_AFTER_SUBDIR_GROUP + ">[^\\s\\[\\]]+)(?<" + VCS_EXTRAS_AFTER_SUBDIR_GROUP + ">" + EXTRAS_REGEXP + ")?" + - ")" + - ")?"; - - @NotNull - private static final String VCS_GROUP = "vcs"; - - @NotNull - private static final String VCS_URL_PREFIX = VCS_SRC_BEFORE_REGEXP + VCS_EDITABLE_REGEXP + "(?<" + VCS_GROUP + ">"; - - @NotNull - private static final String VCS_URL_SUFFIX = - PATH_IN_VCS_REGEXP + VCS_REVISION_REGEXP + VCS_PARAMS_REGEXP + ")" + VCS_SRC_AFTER_REGEXP + COMMENT_REGEXP; - - @NotNull - private static final String GIT_USER_AT_REGEXP = "[\\w-]+@"; - - // supports: git+user@... - @NotNull - private static final Pattern GIT_PROJECT_URL = - Pattern.compile(VCS_URL_PREFIX + "git\\+" + GIT_USER_AT_REGEXP + "[^:\\s]+:" + VCS_URL_SUFFIX); - - // supports: bzr+lp:... - @NotNull - private static final Pattern BZR_PROJECT_URL = Pattern.compile(VCS_URL_PREFIX + "bzr\\+lp:" + VCS_URL_SUFFIX); - - // supports: (bzr|git|hg|svn)(+smth)?://... - @NotNull - private static final Pattern VCS_PROJECT_URL = - Pattern.compile(VCS_URL_PREFIX + "(bzr|git|hg|svn)(\\+[A-Za-z]+)?://?[^/]+/" + VCS_URL_SUFFIX); - - // requirement-related regular expressions - // don't forget to update calculateRequirementInstallOptions(Matcher) after this section changing - - @NotNull - private static final String REQUIREMENT_EXTRAS_GROUP = "extras"; - - @NotNull - private static final String REQUIREMENT_EXTRAS_REGEXP = "(?<" + REQUIREMENT_EXTRAS_GROUP + ">" + EXTRAS_REGEXP + ")?"; - - // PEP-440 - // https://www.python.org/dev/peps/pep-0440/ - - @NotNull - private static final String REQUIREMENT_VERSIONS_SPECS_GROUP = "versionspecs"; - - @NotNull - private static final String REQUIREMENT_VERSION_SPEC_REGEXP = "(<=?|!=|===?|>=?|~=)" + LINE_WS_REGEXP + "*[\\.\\*\\+!\\w-]+"; - - @NotNull - private static final String REQUIREMENT_VERSIONS_SPECS_REGEXP = - "(?<" + REQUIREMENT_VERSIONS_SPECS_GROUP + ">" + REQUIREMENT_VERSION_SPEC_REGEXP + - "(" + LINE_WS_REGEXP + "*," + LINE_WS_REGEXP + "*" + REQUIREMENT_VERSION_SPEC_REGEXP + ")*)?"; - - @NotNull - private static final String REQUIREMENT_OPTIONS_GROUP = "options"; - - @NotNull - private static final String REQUIREMENT_OPTIONS_REGEXP = - "(?<" + REQUIREMENT_OPTIONS_GROUP + ">(" + LINE_WS_REGEXP + "+(--global-option|--install-option)=\"[^\"]*\")+)?"; - - @NotNull - private static final String REQUIREMENT_GROUP = "requirement"; - - @NotNull - private static final Pattern REQUIREMENT = Pattern.compile( - "(?<" + REQUIREMENT_GROUP + ">" + - NAME_REGEXP + - LINE_WS_REGEXP + "*" + - REQUIREMENT_EXTRAS_REGEXP + - LINE_WS_REGEXP + "*" + - REQUIREMENT_VERSIONS_SPECS_REGEXP + - ")" + - REQUIREMENT_OPTIONS_REGEXP + - COMMENT_REGEXP); - - @NotNull - private final String myName; - - @NotNull - private final List myVersionSpecs; - - @NotNull - private final List myInstallOptions; - - @NotNull - private final String myExtras; + List getVersionSpecs(); /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This constructor will be removed in 2018.2. - */ - @Deprecated - public PyRequirement(@NotNull String name) { - this(name, Collections.emptyList()); - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This constructor will be removed in 2018.2. - */ - @Deprecated - public PyRequirement(@NotNull String name, @NotNull PyRequirementRelation relation, @NotNull String version) { - this(name, Collections.singletonList(calculateVersionSpec(version, relation))); - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This constructor will be removed in 2018.2. - */ - @Deprecated - public PyRequirement(@NotNull String name, @NotNull String version) { - this(name, PyRequirementRelation.EQ, version); - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This constructor will be removed in 2018.2. - */ - @Deprecated - public PyRequirement(@NotNull String name, @NotNull String version, @NotNull List installOptions) { - this(name, Collections.singletonList(calculateVersionSpec(version, PyRequirementRelation.EQ)), installOptions); - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This constructor will be removed in 2018.2. - */ - @Deprecated - public PyRequirement(@NotNull String name, @NotNull String version, @NotNull List installOptions, @NotNull String extras) { - this(name, Collections.singletonList(calculateVersionSpec(version, PyRequirementRelation.EQ)), installOptions, extras); - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This constructor will be removed in 2018.2. - */ - @Deprecated - public PyRequirement(@NotNull String name, @NotNull List versionSpecs) { - myName = name; - myVersionSpecs = versionSpecs; - myExtras = ""; - myInstallOptions = Collections.singletonList(toString()); - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This constructor will be removed in 2018.2. - */ - @Deprecated - public PyRequirement(@NotNull String name, @NotNull List versionSpecs, @NotNull List installOptions) { - myName = name; - myVersionSpecs = versionSpecs; - myInstallOptions = Collections.unmodifiableList(installOptions); - myExtras = ""; - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This constructor will be removed in 2018.2. - */ - @Deprecated - public PyRequirement(@NotNull String name, - @NotNull List versionSpecs, - @NotNull List installOptions, - @NotNull String extras) { - myName = name; - myVersionSpecs = versionSpecs; - myInstallOptions = Collections.unmodifiableList(installOptions); - myExtras = extras; - } - - /** - * @deprecated This method will be removed in 2018.2. + * @return list of options to pass to pip install. + * + * Note: + * if list has more than one element it means that + * --src, -e, --editable, --global-option or --install-option + * options are used + * . */ @NotNull - @Deprecated - public PyRequirement withVersionComparator(@NotNull Comparator comparator) { - return new PyRequirement(myName, - ContainerUtil.map(myVersionSpecs, spec -> spec.withVersionComparator(comparator)), myInstallOptions, myExtras); - } + List getInstallOptions(); @NotNull - public String getName() { - return myName; - } + String getExtras(); /** - * @deprecated Use {@link PyRequirement#getName()} and {@link PyRequirement#getExtras()} instead. - * This method will be removed in 2018.2. - */ - @NotNull - @Deprecated - public String getFullName() { - return myName + myExtras; - } - - @NotNull - public String getExtras() { - return myExtras; - } - - @NotNull - public List getVersionSpecs() { - return myVersionSpecs; - } - - @NotNull - public List getInstallOptions() { - return myInstallOptions; - } - - @Override - public String toString() { - return myName + myExtras + StringUtil.join(myVersionSpecs, ","); - } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (o == null || getClass() != o.getClass()) return false; - - final PyRequirement that = (PyRequirement)o; - - if (!myName.equals(that.myName)) return false; - if (!myVersionSpecs.equals(that.myVersionSpecs)) return false; - if (!myInstallOptions.equals(that.myInstallOptions)) return false; - if (!myExtras.equals(that.myExtras)) return false; - - return true; - } - - @Override - public int hashCode() { - int result = myName.hashCode(); - result = 31 * result + myVersionSpecs.hashCode(); - result = 31 * result + myInstallOptions.hashCode(); - result = 31 * result + myExtras.hashCode(); - return result; - } - - @Nullable - public PyPackage match(@NotNull List packages) { - final String normalizedName = normalizeName(myName); - - return packages - .stream() - .filter(pkg -> normalizedName.equalsIgnoreCase(pkg.getName())) - .findAny() - .filter(pkg -> myVersionSpecs.stream().allMatch(spec -> spec.matches(pkg.getVersion()))) - .orElse(null); - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This method will be removed in 2018.2. + * @param packages packages to match + * @return first package that satisfies this requirement or null. */ @Nullable - @Deprecated - public static PyRequirement fromLine(@NotNull String line) { - final PyRequirement githubArchiveUrl = parseGitArchiveUrl(GITHUB_ARCHIVE_URL, line); - if (githubArchiveUrl != null) { - return githubArchiveUrl; - } - - final PyRequirement gitlabArchiveUrl = parseGitArchiveUrl(GITLAB_ARCHIVE_URL, line); - if (gitlabArchiveUrl != null) { - return gitlabArchiveUrl; - } - - final PyRequirement archiveUrl = parseArchiveUrl(line); - if (archiveUrl != null) { - return archiveUrl; - } - - final PyRequirement vcsProjectUrl = parseVcsProjectUrl(line); - if (vcsProjectUrl != null) { - return vcsProjectUrl; - } - - return parseRequirement(line); - } + PyPackage match(@NotNull Collection packages); /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This method will be removed in 2018.2. + * @return concatenated representation of name, extras and version specs so it could be easily displayed. */ @NotNull - @Deprecated - public static List fromText(@NotNull String text) { - return fromText(text, null, new HashSet<>()); - } - - /** - * @deprecated Use {@link PyPackageManager#parseRequirements(String)} instead. - * This method will be removed in 2018.2. - */ - @NotNull - @Deprecated - public static List fromFile(@NotNull VirtualFile file) { - return fromText(loadText(file), file, new HashSet<>()); - } - - /** - * @deprecated Use {@link PyRequirement} instead. - * This method will be removed in 2018.2. - */ - @NotNull - @Deprecated - public static PyRequirementVersionSpec calculateVersionSpec(@NotNull String version, @NotNull PyRequirementRelation expectedRelation) { - if (expectedRelation == PyRequirementRelation.STR_EQ) return new PyRequirementVersionSpec(version); - - final PyRequirementVersion normalizedVersion = PyRequirementVersionNormalizer.normalize(version); - - return normalizedVersion == null ? - new PyRequirementVersionSpec(version) : - new PyRequirementVersionSpec(expectedRelation, normalizedVersion); - } - - @Nullable - private static PyRequirement parseGitArchiveUrl(@NotNull Pattern pattern, @NotNull String line) { - final Matcher matcher = pattern.matcher(line); - - if (matcher.matches()) { - return new PyRequirement(matcher.group(NAME_GROUP), Collections.emptyList(), Collections.singletonList(dropComments(line, matcher))); - } - - return null; - } - - @Nullable - private static PyRequirement parseArchiveUrl(@NotNull String line) { - final Matcher matcher = ARCHIVE_URL.matcher(line); - - if (matcher.matches()) { - return createVcsOrArchiveRequirement(parseNameAndVersionFromVcsOrArchive(matcher.group(NAME_GROUP)), - Collections.singletonList(dropComments(line, matcher)), - null); - } - - return null; - } - - @Nullable - private static PyRequirement parseVcsProjectUrl(@NotNull String line) { - final Matcher vcsMatcher = VCS_PROJECT_URL.matcher(line); - if (vcsMatcher.matches()) { - return createVcsRequirement(vcsMatcher); - } - - final Matcher gitMatcher = GIT_PROJECT_URL.matcher(line); - if (gitMatcher.matches()) { - return createVcsRequirement(gitMatcher); - } - - final Matcher bzrMatcher = BZR_PROJECT_URL.matcher(line); - if (bzrMatcher.matches()) { - return createVcsRequirement(bzrMatcher); - } - - return null; - } - - @Nullable - private static PyRequirement parseRequirement(@NotNull String line) { - final Matcher matcher = REQUIREMENT.matcher(line); - if (matcher.matches()) { - final String name = matcher.group(NAME_GROUP); - final List versionSpecs = parseVersionSpecs(matcher.group(REQUIREMENT_VERSIONS_SPECS_GROUP)); - final List installOptions = calculateRequirementInstallOptions(matcher); - final String extras = matcher.group(REQUIREMENT_EXTRAS_GROUP); - - if (extras == null) { - return new PyRequirement(name, versionSpecs, installOptions); - } - else { - return new PyRequirement(name, versionSpecs, installOptions, extras); - } - } - - return null; - } - - @NotNull - private static List fromText(@NotNull String text, - @Nullable VirtualFile containingFile, - @NotNull Set visitedFiles) { - if (containingFile != null) { - visitedFiles.add(containingFile); - } - - return StreamEx - .of(splitByLinesAndCollapse(text)) - .flatCollection(line -> parseLine(line, containingFile, visitedFiles)) - .nonNull() - .distinct() - .toList(); - } - - @NotNull - private static String loadText(@NotNull VirtualFile file) { - final Document document = FileDocumentManager.getInstance().getDocument(file); - - return document == null ? "" : document.getText(); - } - - @NotNull - private static String dropComments(@NotNull String line, @NotNull Matcher matcher) { - final int commentIndex = matcher.start(COMMENT_GROUP); - - if (commentIndex == -1) { - return line; - } - - return line.substring(0, findFirstNotWhiteSpaceBefore(line, commentIndex) + 1); - } - - @NotNull - private static Pair parseNameAndVersionFromVcsOrArchive(@NotNull String name) { - boolean isName = true; - final List nameParts = new ArrayList<>(); - final List versionParts = new ArrayList<>(); - - for (String part : StringUtil.split(name, "-")) { - final boolean partStartsWithDigit = !part.isEmpty() && Character.isDigit(part.charAt(0)); - - if (partStartsWithDigit || "dev".equals(part)) { - isName = false; - } - - if (isName) { - nameParts.add(part); - } - else { - versionParts.add(part); - } - } - - return Pair.create(normalizeVcsOrArchiveNameParts(nameParts), normalizeVcsOrArchiveVersionParts(versionParts)); - } - - @NotNull - private static PyRequirement createVcsOrArchiveRequirement(@NotNull Pair nameAndVersion, - @NotNull List installOptions, - @Nullable String extras) { - final String name = nameAndVersion.getFirst(); - final String version = nameAndVersion.getSecond(); - - if (version == null) { - if (extras == null) { - return new PyRequirement(name, Collections.emptyList(), installOptions); - } - else { - return new PyRequirement(name, Collections.emptyList(), installOptions, extras); - } - } - - final List versionSpecs = Collections.singletonList(calculateVersionSpec(version, PyRequirementRelation.EQ)); - if (extras == null) { - return new PyRequirement(name, versionSpecs, installOptions); - } - else { - return new PyRequirement(name, versionSpecs, installOptions, extras); - } - } - - @NotNull - private static PyRequirement createVcsRequirement(@NotNull Matcher matcher) { - final String path = matcher.group(PATH_IN_VCS_GROUP); - final String egg = getEgg(matcher); - - final String project = extractProject(dropTrunk(dropRevision(path))); - final Pair nameAndVersion = - parseNameAndVersionFromVcsOrArchive(egg == null ? StringUtil.trimEnd(project, ".git") : egg); - - return createVcsOrArchiveRequirement(nameAndVersion, calculateVcsInstallOptions(matcher), getVcsExtras(matcher)); - } - - @NotNull - private static List parseVersionSpecs(@Nullable String versionSpecs) { - if (versionSpecs == null) return Collections.emptyList(); - - return StreamSupport - .stream(StringUtil.tokenize(versionSpecs, ",").spliterator(), false) - .map(String::trim) - .map(PyRequirement::parseVersionSpec) - .filter(req -> req != null) - .collect(Collectors.toList()); - } - - @NotNull - private static List calculateRequirementInstallOptions(@NotNull Matcher matcher) { - final List result = new ArrayList<>(); - result.add(matcher.group(REQUIREMENT_GROUP)); - - final String requirementOptions = matcher.group(REQUIREMENT_OPTIONS_GROUP); - if (requirementOptions != null) { - boolean isKey = true; - for (String token : StringUtil.tokenize(requirementOptions, "\"")) { - result.add(isKey ? token.substring(findFirstNotWhiteSpaceAfter(token, 0), token.length() - 1) : token); - isKey = !isKey; - } - } - - return result; - } - - @NotNull - private static List splitByLinesAndCollapse(@NotNull String text) { - final List result = new ArrayList<>(); - final StringBuilder sb = new StringBuilder(); - - for (String line : StringUtil.splitByLines(text)) { - if (line.endsWith("\\") && !line.endsWith("\\\\")) { - sb.append(line, 0, line.length() - 1); - } - else { - if (sb.length() == 0) { - result.add(line); - } - else { - sb.append(line); - - result.add(sb.toString()); - - sb.setLength(0); - } - } - } - - return result; - } - - @NotNull - private static List parseLine(@NotNull String line, - @Nullable VirtualFile containingFile, - @NotNull Set visitedFiles) { - if (line.startsWith("-r")) { - return parseRecursiveLine(line, containingFile, visitedFiles, "-r".length()); - } - - if (line.startsWith("--requirement ")) { - return parseRecursiveLine(line, containingFile, visitedFiles, "--requirement ".length()); - } - - return Collections.singletonList(fromLine(line)); - } - - @NotNull - private static String normalizeVcsOrArchiveNameParts(@NotNull List nameParts) { - return normalizeName(StringUtil.join(nameParts, "-")); - } - - @Nullable - private static String normalizeVcsOrArchiveVersionParts(@NotNull List versionParts) { - return versionParts.isEmpty() ? null : normalizeVersion(StringUtil.join(versionParts, "-")); - } - - @NotNull - private static List calculateVcsInstallOptions(@NotNull Matcher matcher) { - final List result = new ArrayList<>(); - - final String srcBefore = matcher.group(VCS_SRC_BEFORE_GROUP); - if (srcBefore != null) { - result.addAll(Arrays.asList(srcBefore.split("\\s+"))); - } - - final String editable = matcher.group(VCS_EDITABLE_GROUP); - if (editable != null) { - result.add(editable); - } - - result.add(matcher.group(VCS_GROUP)); - - final String srcAfter = matcher.group(VCS_SRC_AFTER_GROUP); - if (srcAfter != null) { - result.addAll(Arrays.asList(srcAfter.split("\\s+")).subList(1, 3)); // skip spaces before --src and get only two values - } - - return result; - } - - @Nullable - private static String getEgg(@NotNull Matcher matcher) { - final String beforeSubdir = matcher.group(VCS_EGG_BEFORE_SUBDIR_GROUP); - - return beforeSubdir == null ? matcher.group(VCS_EGG_AFTER_SUBDIR_GROUP) : beforeSubdir; - } - - @NotNull - private static String extractProject(@NotNull String path) { - final int end = path.endsWith("/") ? path.length() - 1 : path.length(); - final int slashIndex = path.lastIndexOf("/", end - 1); - - if (slashIndex != -1) { - return path.substring(slashIndex + 1, end); - } - - if (end != path.length()) { - return path.substring(0, end); - } - - return path; - } - - @NotNull - private static String dropTrunk(@NotNull String path) { - final String slashTrunk = "/trunk"; - - if (path.endsWith(slashTrunk)) { - return path.substring(0, path.length() - slashTrunk.length()); - } - - final String slashTrunkSlash = "/trunk/"; - - if (path.endsWith(slashTrunkSlash)) { - return path.substring(0, path.length() - slashTrunkSlash.length()); - } - - return path; - } - - @NotNull - private static String dropRevision(@NotNull String path) { - final int atIndex = path.lastIndexOf("@"); - - if (atIndex != -1) { - return path.substring(0, atIndex); - } - - return path; - } - - @Nullable - private static String getVcsExtras(@NotNull Matcher matcher) { - final String beforeSubdir = matcher.group(VCS_EXTRAS_BEFORE_SUBDIR_GROUP); - - return beforeSubdir == null ? matcher.group(VCS_EXTRAS_AFTER_SUBDIR_GROUP) : beforeSubdir; - } - - @Nullable - private static PyRequirementVersionSpec parseVersionSpec(@NotNull String versionSpec) { - PyRequirementRelation relation = null; - - if (versionSpec.startsWith("===")) { - relation = PyRequirementRelation.STR_EQ; - } - else if (versionSpec.startsWith("==")) { - relation = PyRequirementRelation.EQ; - } - else if (versionSpec.startsWith("<=")) { - relation = PyRequirementRelation.LTE; - } - else if (versionSpec.startsWith(">=")) { - relation = PyRequirementRelation.GTE; - } - else if (versionSpec.startsWith("<")) { - relation = PyRequirementRelation.LT; - } - else if (versionSpec.startsWith(">")) { - relation = PyRequirementRelation.GT; - } - else if (versionSpec.startsWith("~=")) { - relation = PyRequirementRelation.COMPATIBLE; - } - else if (versionSpec.startsWith("!=")) { - relation = PyRequirementRelation.NE; - } - - if (relation != null) { - final int versionIndex = findFirstNotWhiteSpaceAfter(versionSpec, relation.toString().length()); - return calculateVersionSpec(versionSpec.substring(versionIndex), relation); - } - - return null; - } - - @NotNull - private static List parseRecursiveLine(@NotNull String line, - @Nullable VirtualFile containingFile, - @NotNull Set visitedFiles, - int flagLength) { - if (containingFile == null) return Collections.emptyList(); - - final int pathIndex = findFirstNotWhiteSpaceAfter(line, flagLength); - if (pathIndex == line.length()) return Collections.emptyList(); - - final String path = FileUtil.toSystemIndependentName(line.substring(pathIndex)); - final VirtualFile file = findRecursiveFile(containingFile, path); - - if (file != null && !visitedFiles.contains(file)) { - return fromText(loadText(file), file, visitedFiles); - } - - return Collections.emptyList(); - } - - @NotNull - private static String normalizeName(@NotNull String s) { - return s.replace("_", "-"); - } - - @NotNull - private static String normalizeVersion(@NotNull String s) { - return s.replace("_", "-").replaceAll("-?py[\\d\\.]+", ""); - } - - private static int findFirstNotWhiteSpaceAfter(@NotNull String line, int beginIndex) { - for (int i = beginIndex; i < line.length(); i++) { - if (!StringUtil.isWhiteSpace(line.charAt(i))) { - return i; - } - } - - return line.length(); - } - - private static int findFirstNotWhiteSpaceBefore(@NotNull String line, int beginIndex) { - for (int i = beginIndex; i >= 0; i--) { - if (!StringUtil.isWhiteSpace(line.charAt(i))) { - return i; - } - } - - return -1; - } - - @Nullable - private static VirtualFile findRecursiveFile(@NotNull VirtualFile containingFile, @NotNull String path) { - final VirtualFile dir = containingFile.getParent(); - if (dir == null) return null; - - final VirtualFile file = dir.findFileByRelativePath(path); - if (file != null) return file; - - return LocalFileSystem.getInstance().findFileByPath(path); + default String getPresentableText() { + return getName() + getExtras() + StringUtil.join(getVersionSpecs(), PyRequirementVersionSpec::getPresentableText, ","); } } diff --git a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementRelation.java b/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementRelation.java index dbde55e84c5d..ed3e5507cf9b 100644 --- a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementRelation.java +++ b/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementRelation.java @@ -15,6 +15,11 @@ package com.jetbrains.python.packaging.requirement; import org.jetbrains.annotations.NotNull; +/** + * @see PEP-440 + * @see PyRequirementVersionSpec + * @see com.jetbrains.python.packaging.PyRequirement + */ public enum PyRequirementRelation { LT("<"), @@ -33,9 +38,11 @@ public enum PyRequirementRelation { myValue = value; } + /** + * @return representation of this relation that is used in PEP-440. + */ @NotNull - @Override - public String toString() { + public String getPresentableText() { return myValue; } } diff --git a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersion.kt b/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersion.kt deleted file mode 100644 index d32f67c35f56..000000000000 --- a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersion.kt +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2000-2017 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 - -@Deprecated(message = "Use com.jetbrains.python.packaging.PyRequirement instead. This class will be removed in 2018.2.") -data class PyRequirementVersion(val epoch: String? = null, - val release: String, - val pre: String? = null, - val post: String? = null, - val dev: String? = null, - val local: String? = null) { - - companion object { - @JvmStatic - fun release(release: String): PyRequirementVersion = PyRequirementVersion(release = release) - } - - val presentableText: String - get() = - sequenceOf(epochPresentable(), release, pre, postPresentable(), devPresentable(), localPresentable()) - .filterNotNull() - .joinToString(separator = "") { it } - - private fun epochPresentable() = if (epoch == null) null else "$epoch!" - private fun postPresentable() = if (post == null) null else ".$post" - private fun devPresentable() = if (dev == null) null else ".$dev" - private fun localPresentable() = if (local == null) null else "+$local" -} \ No newline at end of file diff --git a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionSpec.java b/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionSpec.java index 49e642440023..dd69157f7969 100644 --- a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionSpec.java +++ b/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionSpec.java @@ -1,172 +1,32 @@ -// Copyright 2000-2017 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. +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.packaging.requirement; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Comparator; -import java.util.Objects; - -import static com.intellij.webcore.packaging.PackageVersionComparator.VERSION_COMPARATOR; /** - * @apiNote This class will be converted to interface in 2018.2. + * @see PEP-440 + * @see com.jetbrains.python.packaging.PyRequirement + * @see PyRequirementRelation */ -public class PyRequirementVersionSpec { +public interface PyRequirementVersionSpec { @NotNull - private final PyRequirementRelation myRelation; - - @Nullable - private final PyRequirementVersion myParsedVersion; + PyRequirementRelation getRelation(); @NotNull - private final String myVersion; - - @NotNull - private final Comparator myVersionComparator; + String getVersion(); /** - * @deprecated Use {@link com.jetbrains.python.packaging.PyRequirement} instead. - * This constructor will be removed in 2018.2. + * @param version version to check + * @return true if given version satisfies this version spec. */ - public PyRequirementVersionSpec(@NotNull PyRequirementRelation relation, @NotNull PyRequirementVersion version) { - this(relation, version, version.getPresentableText(), VERSION_COMPARATOR); - } + boolean matches(@NotNull String version); /** - * @deprecated Use {@link com.jetbrains.python.packaging.PyRequirement} instead. - * This constructor will be removed in 2018.2. - */ - public PyRequirementVersionSpec(@NotNull String version) { - this(PyRequirementRelation.STR_EQ, null, version, VERSION_COMPARATOR); - } - - private PyRequirementVersionSpec(@NotNull PyRequirementRelation relation, - @Nullable PyRequirementVersion parsedVersion, - @NotNull String version, - @NotNull Comparator versionComparator) { - myRelation = relation; - myParsedVersion = parsedVersion; - myVersion = version; - myVersionComparator = versionComparator; - } - - /** - * @deprecated This method will be removed in 2018.2. + * @return concatenated representation of relation and version so it could be easily parsed or displayed. */ @NotNull - @Deprecated - public PyRequirementVersionSpec withVersionComparator(@NotNull Comparator comparator) { - return new PyRequirementVersionSpec(myRelation, myParsedVersion, myVersion, comparator); - } - - @Override - public String toString() { - return myRelation + myVersion; - } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (o == null || getClass() != o.getClass()) return false; - - final PyRequirementVersionSpec spec = (PyRequirementVersionSpec)o; - return myRelation == spec.myRelation && myVersion.equals(spec.myVersion); - } - - @Override - public int hashCode() { - return 31 * myRelation.hashCode() + myVersion.hashCode(); - } - - @NotNull - public PyRequirementRelation getRelation() { - return myRelation; - } - - @NotNull - public String getVersion() { - return myVersion; - } - - public boolean matches(@NotNull String version) { - switch (myRelation) { - case LT: - return myVersionComparator.compare(version, myVersion) < 0; - case LTE: - return myVersionComparator.compare(version, myVersion) <= 0; - case GT: - return myVersionComparator.compare(version, myVersion) > 0; - case GTE: - return myVersionComparator.compare(version, myVersion) >= 0; - case EQ: - Objects.requireNonNull(myParsedVersion); - - final Pair publicAndLocalVersions = splitIntoPublicAndLocalVersions(myParsedVersion); - final Pair otherPublicAndLocalVersions = splitIntoPublicAndLocalVersions(version); - final boolean publicVersionsAreSame = - myVersionComparator.compare(otherPublicAndLocalVersions.first, publicAndLocalVersions.first) == 0; - - return publicVersionsAreSame && - (publicAndLocalVersions.second.isEmpty() || otherPublicAndLocalVersions.second.equals(publicAndLocalVersions.second)); - case NE: - return myVersionComparator.compare(version, myVersion) != 0; - case COMPATIBLE: - Objects.requireNonNull(myParsedVersion); - - return new PyRequirementVersionSpec(PyRequirementRelation.GTE, myParsedVersion) - .withVersionComparator(myVersionComparator) - .matches(version) && - new PyRequirementVersionSpec(PyRequirementRelation.EQ, toEqPartOfCompatibleRelation(myParsedVersion)) - .withVersionComparator(myVersionComparator) - .matches(version); - case STR_EQ: - return version.equals(myVersion); - default: - return false; - } - } - - @NotNull - private static Pair splitIntoPublicAndLocalVersions(@NotNull PyRequirementVersion version) { - final PyRequirementVersion withoutLocal = - new PyRequirementVersion(version.getEpoch(), version.getRelease(), version.getPre(), version.getPost(), version.getDev(), null); - - return Pair.createNonNull(withoutLocal.getPresentableText(), StringUtil.notNullize(version.getLocal())); - } - - @NotNull - private static Pair splitIntoPublicAndLocalVersions(@NotNull String version) { - final String[] publicAndLocalVersions = version.split("\\+", 2); - - final String publicVersion = publicAndLocalVersions[0]; - final String localVersion = publicAndLocalVersions.length == 1 ? "" : publicAndLocalVersions[1]; - - return Pair.createNonNull(publicVersion, localVersion); - } - - @NotNull - private static PyRequirementVersion toEqPartOfCompatibleRelation(@NotNull PyRequirementVersion version) { - final String release = version.getRelease(); - final int lastPoint = release.lastIndexOf('.'); - - if (lastPoint == -1) return version; - - return new PyRequirementVersion(version.getEpoch(), release.substring(0, lastPoint + 1) + "*", null, null, null, null); + default String getPresentableText() { + return getRelation().getPresentableText() + getVersion(); } } diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java index c519eda530ce..bf37d3585814 100644 --- a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java @@ -46,7 +46,7 @@ import com.jetbrains.python.documentation.docstrings.DocStringFormat; import com.jetbrains.python.packaging.PyPackageManagerUI; import com.jetbrains.python.packaging.PyPackageRequirementsSettings; import com.jetbrains.python.packaging.PyPackageUtil; -import com.jetbrains.python.packaging.PyRequirement; +import com.jetbrains.python.packaging.PyRequirementsKt; import com.jetbrains.python.psi.PyUtil; import com.jetbrains.python.sdk.PythonSdkType; import com.jetbrains.python.testing.PyTestFrameworkService; @@ -164,7 +164,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable { } } }); - ui.install(Collections.singletonList(new PyRequirement(name)), Collections.emptyList()); + ui.install(Collections.singletonList(PyRequirementsKt.pyRequirement(name)), Collections.emptyList()); } }; } diff --git a/python/src/com/jetbrains/python/inspections/PyPackageRequirementsInspection.java b/python/src/com/jetbrains/python/inspections/PyPackageRequirementsInspection.java index 25a1b836c256..b078b055f7ad 100644 --- a/python/src/com/jetbrains/python/inspections/PyPackageRequirementsInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyPackageRequirementsInspection.java @@ -108,7 +108,7 @@ public class PyPackageRequirementsInspection extends PyInspection { plural ? "are" : "is"); final Set unsatisfiedNames = new HashSet<>(); for (PyRequirement req : unsatisfied) { - unsatisfiedNames.add(req.getFullName()); + unsatisfiedNames.add(req.getName() + req.getExtras()); } final List quickFixes = new ArrayList<>(); quickFixes.add(new PyInstallRequirementsFix(null, module, sdk, unsatisfied)); @@ -484,7 +484,7 @@ public class PyPackageRequirementsInspection extends PyInspection { } } }); - ui.install(Collections.singletonList(new PyRequirement(myPackageName)), Collections.emptyList()); + ui.install(Collections.singletonList(PyRequirementsKt.pyRequirement(myPackageName)), Collections.emptyList()); } } diff --git a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java index 7eb05d88ad16..c4b41b9ad620 100644 --- a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java @@ -46,6 +46,7 @@ import com.jetbrains.python.inspections.quickfix.*; import com.jetbrains.python.packaging.PyPIPackageUtil; import com.jetbrains.python.packaging.PyPackageUtil; import com.jetbrains.python.packaging.PyRequirement; +import com.jetbrains.python.packaging.PyRequirementsKt; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyImportStatementNavigator; @@ -683,7 +684,7 @@ public class PyUnresolvedReferencesInspection extends PyInspection { } private static void addInstallPackageAction(List actions, String packageName, Module module, Sdk sdk) { - final List requirements = Collections.singletonList(new PyRequirement(packageName)); + final List requirements = Collections.singletonList(PyRequirementsKt.pyRequirement(packageName)); final String name = "Install package " + packageName; actions.add(new PyPackageRequirementsInspection.PyInstallRequirementsFix(name, module, sdk, requirements)); } diff --git a/python/src/com/jetbrains/python/packaging/PyCondaPackageManagerImpl.java b/python/src/com/jetbrains/python/packaging/PyCondaPackageManagerImpl.java index dab99663f97e..1bf063047253 100644 --- a/python/src/com/jetbrains/python/packaging/PyCondaPackageManagerImpl.java +++ b/python/src/com/jetbrains/python/packaging/PyCondaPackageManagerImpl.java @@ -61,7 +61,7 @@ public class PyCondaPackageManagerImpl extends PyPackageManagerImpl { if (useConda) { final ArrayList arguments = new ArrayList<>(); for (PyRequirement requirement : requirements) { - arguments.add(requirement.toString()); + arguments.add(requirement.getPresentableText()); } arguments.add("-y"); if (!extraArgs.contains("-U")) { @@ -154,7 +154,7 @@ public class PyCondaPackageManagerImpl extends PyPackageManagerImpl { } @NotNull - protected static List parseCondaToolOutput(@NotNull String s) throws ExecutionException { + private List parseCondaToolOutput(@NotNull String s) throws ExecutionException { final String[] lines = StringUtil.splitByLines(s); final List packages = new ArrayList<>(); for (String line : lines) { @@ -169,7 +169,7 @@ public class PyCondaPackageManagerImpl extends PyPackageManagerImpl { if (fields.size() >= 4) { final String requiresLine = fields.get(3); final String requiresSpec = StringUtil.join(StringUtil.split(requiresLine, ":"), "\n"); - requirements.addAll(PyPackageUtil.fix(PyRequirement.fromText(requiresSpec))); + requirements.addAll(parseRequirements(requiresSpec)); } if (!"Python".equals(name)) { packages.add(new PyPackage(name, version, "", requirements)); diff --git a/python/src/com/jetbrains/python/packaging/PyPackageManagerImpl.java b/python/src/com/jetbrains/python/packaging/PyPackageManagerImpl.java index 48f953f9a4f3..d0767b075c7e 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageManagerImpl.java +++ b/python/src/com/jetbrains/python/packaging/PyPackageManagerImpl.java @@ -174,7 +174,7 @@ public class PyPackageManagerImpl extends PyPackageManager { @Override public void install(@NotNull String requirementString) throws ExecutionException { - install(Collections.singletonList(PyRequirement.fromLine(requirementString)), Collections.emptyList()); + install(Collections.singletonList(parseRequirement(requirementString)), Collections.emptyList()); } @Override @@ -391,10 +391,22 @@ public class PyPackageManagerImpl extends PyPackageManager { .orElseGet(() -> PyPackageUtil.findSetupPyRequires(module)); } + @Nullable + @Override + public PyRequirement parseRequirement(@NotNull String line) { + return PyRequirementParser.fromLine(line); + } + @NotNull @Override public List parseRequirements(@NotNull String text) { - return PyPackageUtil.fix(PyRequirement.fromText(text)); + return PyRequirementParser.fromText(text); + } + + @NotNull + @Override + public List parseRequirements(@NotNull VirtualFile file) { + return PyRequirementParser.fromFile(file); } // public List refreshAndGetPackagesIfNotInProgress(boolean alwaysRefresh) throws ExecutionException @@ -547,7 +559,7 @@ public class PyPackageManagerImpl extends PyPackageManager { } @NotNull - private static List parsePackagingToolOutput(@NotNull String s) throws ExecutionException { + private List parsePackagingToolOutput(@NotNull String s) throws ExecutionException { final String[] lines = StringUtil.splitByLines(s); final List packages = new ArrayList<>(); for (String line : lines) { @@ -562,7 +574,7 @@ public class PyPackageManagerImpl extends PyPackageManager { if (fields.size() >= 4) { final String requiresLine = fields.get(3); final String requiresSpec = StringUtil.join(StringUtil.split(requiresLine, ":"), "\n"); - requirements.addAll(PyPackageUtil.fix(PyRequirement.fromText(requiresSpec))); + requirements.addAll(parseRequirements(requiresSpec)); } if (!"Python".equals(name)) { packages.add(new PyPackage(name, version, location, requirements)); diff --git a/python/src/com/jetbrains/python/packaging/PyPackageUtil.java b/python/src/com/jetbrains/python/packaging/PyPackageUtil.java index 0e18482cd091..3bdf6eacc14b 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageUtil.java +++ b/python/src/com/jetbrains/python/packaging/PyPackageUtil.java @@ -37,7 +37,6 @@ import com.intellij.openapi.vfs.VirtualFileVisitor; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; -import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyBundle; import com.jetbrains.python.PyNames; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; @@ -49,6 +48,7 @@ import com.jetbrains.python.psi.types.TypeEvalContext; import com.jetbrains.python.remote.PyCredentialsContribution; import com.jetbrains.python.sdk.CredentialsTypeExChecker; import com.jetbrains.python.sdk.PythonSdkType; +import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -125,30 +125,24 @@ public class PyPackageUtil { } @Nullable - private static PyListLiteralExpression findSetupPyInstallRequires(@NotNull Module module, @Nullable PyCallExpression setupCall) { - if (setupCall == null) { - return null; - } + private static PyListLiteralExpression findSetupPyInstallRequires(@Nullable PyCallExpression setupCall) { + if (setupCall == null) return null; - return Stream + return StreamEx .of(REQUIRES, INSTALL_REQUIRES) .map(setupCall::getKeywordArgument) - .map(requires -> resolveValue(module, requires, PyListLiteralExpression.class)) - .filter(requires -> requires != null) - .findFirst() + .map(requires -> resolveValue(requires, PyListLiteralExpression.class)) + .findFirst(Objects::nonNull) .orElse(null); } @Nullable public static List findSetupPyRequires(@NotNull Module module) { final PyCallExpression setupCall = findSetupCall(module); + if (setupCall == null) return null; - if (setupCall == null) { - return null; - } - - final List requirementsFromRequires = getSetupPyRequiresFromArguments(module, setupCall, SETUP_PY_REQUIRES_KWARGS_NAMES); - final List requirementsFromLinks = getSetupPyRequiresFromArguments(module, setupCall, DEPENDENCY_LINKS); + final List requirementsFromRequires = getSetupPyRequiresFromArguments(setupCall, SETUP_PY_REQUIRES_KWARGS_NAMES); + final List requirementsFromLinks = getSetupPyRequiresFromArguments(setupCall, DEPENDENCY_LINKS); return mergeSetupPyRequirements(requirementsFromRequires, requirementsFromLinks); } @@ -159,7 +153,7 @@ public class PyPackageUtil { if (setupCall == null) return null; final PyDictLiteralExpression extrasRequire = - resolveValue(module, setupCall.getKeywordArgument("extras_require"), PyDictLiteralExpression.class); + resolveValue(setupCall.getKeywordArgument("extras_require"), PyDictLiteralExpression.class); if (extrasRequire == null) return null; final Map> result = new HashMap<>(); @@ -191,8 +185,7 @@ public class PyPackageUtil { } if (requiresValue != null) { - return Pair.createNonNull(((PyStringLiteralExpression)extra).getStringValue(), - fix(PyRequirement.fromText((requiresValue)))); + return Pair.createNonNull(((PyStringLiteralExpression)extra).getStringValue(), PyRequirementParser.fromText(requiresValue)); } } @@ -200,42 +193,21 @@ public class PyPackageUtil { } @NotNull - private static List getSetupPyRequiresFromArguments(@NotNull Module module, - @NotNull PyCallExpression setupCall, + private static List getSetupPyRequiresFromArguments(@NotNull PyCallExpression setupCall, @NotNull String... argumentNames) { - return fix( - PyRequirement.fromText( - Stream - .of(argumentNames) - .map(setupCall::getKeywordArgument) - .map(requires -> resolveValue(module, requires, PyListLiteralExpression.class)) - .filter(requires -> requires != null) - .flatMap(requires -> Stream.of(requires.getElements())) - .filter(PyStringLiteralExpression.class::isInstance) - .map(requirement -> ((PyStringLiteralExpression)requirement).getStringValue()) - .collect(Collectors.joining("\n")) - ) + return PyRequirementParser.fromText( + StreamEx + .of(argumentNames) + .map(setupCall::getKeywordArgument) + .map(requires -> resolveValue(requires, PyListLiteralExpression.class)) + .nonNull() + .flatMap(requires -> Stream.of(requires.getElements())) + .select(PyStringLiteralExpression.class) + .map(StringLiteralExpression::getStringValue) + .joining("\n") ); } - /** - * @deprecated This method will be removed in 2018.2. - */ - @NotNull - @Deprecated - public static PyRequirement fix(@NotNull PyRequirement requirement) { - return requirement.withVersionComparator(PyPackageVersionComparator.getSTR_COMPARATOR()); - } - - /** - * @deprecated This method will be removed in 2018.2. - */ - @NotNull - @Deprecated - public static List fix(@NotNull List requirements) { - return ContainerUtil.map(requirements, PyPackageUtil::fix); - } - @NotNull private static List mergeSetupPyRequirements(@NotNull List requirementsFromRequires, @NotNull List requirementsFromLinks) { @@ -254,12 +226,12 @@ public class PyPackageUtil { } @Nullable - private static T resolveValue(@NotNull Module module, @Nullable PyExpression expression, @NotNull Class cls) { + private static T resolveValue(@Nullable PyExpression expression, @NotNull Class cls) { if (cls.isInstance(expression)) { return cls.cast(expression); } if (expression instanceof PyReferenceExpression) { - final TypeEvalContext context = TypeEvalContext.deepCodeInsight(module.getProject()); + final TypeEvalContext context = TypeEvalContext.deepCodeInsight(expression.getProject()); final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); final QualifiedResolveResult result = ((PyReferenceExpression)expression).followAssignmentsChain(resolveContext); final PsiElement element = result.getElement(); @@ -287,7 +259,7 @@ public class PyPackageUtil { @NotNull public static String requirementsToString(@NotNull List requirements) { - return StringUtil.join(requirements, requirement -> String.format("'%s'", requirement.toString()), ", "); + return StringUtil.join(requirements, requirement -> String.format("'%s'", requirement.getPresentableText()), ", "); } @Nullable @@ -431,7 +403,7 @@ public class PyPackageUtil { public static List getRequirementsFromTxt(@NotNull Module module) { final VirtualFile requirementsTxt = findRequirementsTxt(module); if (requirementsTxt != null) { - return fix(PyRequirement.fromFile(requirementsTxt)); + return PyRequirementParser.fromFile(requirementsTxt); } return null; } @@ -454,7 +426,7 @@ public class PyPackageUtil { } final PyCallExpression setupCall = findSetupCall(setupPy); - final PyListLiteralExpression installRequires = findSetupPyInstallRequires(module, setupCall); + final PyListLiteralExpression installRequires = findSetupPyInstallRequires(setupCall); final PyElementGenerator generator = PyElementGenerator.getInstance(module.getProject()); if (installRequires != null && installRequires.isWritable()) { diff --git a/python/src/com/jetbrains/python/packaging/PyPackageVersion.kt b/python/src/com/jetbrains/python/packaging/PyPackageVersion.kt index ef3e200b57d4..bf7f4cdc0aeb 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageVersion.kt +++ b/python/src/com/jetbrains/python/packaging/PyPackageVersion.kt @@ -1,19 +1,29 @@ // Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.packaging -import com.jetbrains.python.packaging.requirement.PyRequirementVersion - /** - * Presents normalized version of python package or requirement as described [here][https://www.python.org/dev/peps/pep-0440/#normalization]. + * Presents normalized version of python package or requirement as described in [PEP-440][https://www.python.org/dev/peps/pep-0440/#normalization]. * - * Instances of this class MUST be converted from [com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer.normalize] result. + * Instances of this class MUST be obtained from [PyPackageVersionNormalizer.normalize]. */ -data class PyPackageVersion(val epoch: String? = null, - val release: String, - val pre: String? = null, - val post: String? = null, - val dev: String? = null, - val local: String? = null) { +data class PyPackageVersion internal constructor(val epoch: String? = null, + val release: String, + val pre: String? = null, + val post: String? = null, + val dev: String? = null, + val local: String? = null) { - override fun toString(): String = PyRequirementVersion(epoch, release, pre, post, dev, local).presentableText + /** + * String representation that follows spelling described in [PEP-440][https://www.python.org/dev/peps/pep-0440/#normalization] + */ + val presentableText + get() = + sequenceOf(epochPresentable(), release, pre, postPresentable(), devPresentable(), localPresentable()) + .filterNotNull() + .joinToString(separator = "") { it } + + private fun epochPresentable() = epoch?.let { "$it!" } + private fun postPresentable() = post?.let { ".$it" } + private fun devPresentable() = dev?.let { ".$it" } + private fun localPresentable() = local?.let { "+$it" } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/PyPackageVersionComparator.kt b/python/src/com/jetbrains/python/packaging/PyPackageVersionComparator.kt index 186f314ef8f5..d609d617226d 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageVersionComparator.kt +++ b/python/src/com/jetbrains/python/packaging/PyPackageVersionComparator.kt @@ -3,26 +3,27 @@ package com.jetbrains.python.packaging import com.intellij.webcore.packaging.PackageVersionComparator import com.jetbrains.python.packaging.requirement.PyRequirementRelation -import com.jetbrains.python.packaging.requirement.PyRequirementVersion -import com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer import one.util.streamex.EntryStream import one.util.streamex.StreamEx import java.math.BigInteger import java.util.stream.Stream /** - * Compares normalized [PyPackageVersions][PyPackageVersion]. + * Compares [PyPackageVersions][PyPackageVersion]. * * Based on [PEP-440][https://www.python.org/dev/peps/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering]. */ object PyPackageVersionComparator : Comparator { + /** + * Compares versions by normalizing them or using [com.intellij.webcore.packaging.PackageVersionComparator.VERSION_COMPARATOR] as a fallback + */ @JvmStatic val STR_COMPARATOR: Comparator = Comparator { o1, o2 -> - val normalized1 = PyRequirementVersionNormalizer.normalize(o1)?.toPkgVersion() + val normalized1 = PyPackageVersionNormalizer.normalize(o1) ?: return@Comparator PackageVersionComparator.VERSION_COMPARATOR.compare(o1, o2) - val normalized2 = PyRequirementVersionNormalizer.normalize(o2)?.toPkgVersion() + val normalized2 = PyPackageVersionNormalizer.normalize(o2) ?: return@Comparator PackageVersionComparator.VERSION_COMPARATOR.compare(o1, o2) compare(normalized1, normalized2) @@ -51,16 +52,14 @@ object PyPackageVersionComparator : Comparator { return compareLocals(o1, o2) } - private fun PyRequirementVersion.toPkgVersion() = PyPackageVersion(epoch, release, pre, post, dev, local) - /** - * @see com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer.normalizeEpoch + * @see PyPackageVersionNormalizer.normalizeEpoch */ private fun compareEpochs(o1: PyPackageVersion, o2: PyPackageVersion) = compareAsInts( o1.epoch ?: "0", o2.epoch ?: "0") /** - * @see com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer.normalizeRelease + * @see PyPackageVersionNormalizer.normalizeRelease */ private fun compareReleases(o1: PyPackageVersion, o2: PyPackageVersion): PyRequirementRelation { for ((releasePart1, releasePart2) in zipLongest(o1.release.split('.'), @@ -88,7 +87,7 @@ object PyPackageVersionComparator : Comparator { } /** - * @see com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer.normalizePost + * @see PyPackageVersionNormalizer.normalizePost */ private fun comparePosts(o1: PyPackageVersion, o2: PyPackageVersion): Int { return compareAsInts(o1.post?.substring(4) ?: "-1", @@ -96,7 +95,7 @@ object PyPackageVersionComparator : Comparator { } /** - * @see com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer.normalizePre + * @see PyPackageVersionNormalizer.normalizePre */ private fun comparePres(o1: PyPackageVersion, o2: PyPackageVersion): Int { val pre1 = o1.pre @@ -116,7 +115,7 @@ object PyPackageVersionComparator : Comparator { } /** - * @see com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer.normalizeDev + * @see PyPackageVersionNormalizer.normalizeDev */ private fun compareDevs(o1: PyPackageVersion, o2: PyPackageVersion): Int { val dev1 = o1.dev @@ -130,7 +129,7 @@ object PyPackageVersionComparator : Comparator { } /** - * @see com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer.normalizeLocal + * @see PyPackageVersionNormalizer.normalizeLocal */ private fun compareLocals(o1: PyPackageVersion, o2: PyPackageVersion) = (o1.local ?: "").compareTo(o2.local ?: "") diff --git a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionNormalizer.java b/python/src/com/jetbrains/python/packaging/PyPackageVersionNormalizer.java similarity index 86% rename from python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionNormalizer.java rename to python/src/com/jetbrains/python/packaging/PyPackageVersionNormalizer.java index 213c4651a7d6..5eeca64574d8 100644 --- a/python/openapi/src/com/jetbrains/python/packaging/requirement/PyRequirementVersionNormalizer.java +++ b/python/src/com/jetbrains/python/packaging/PyPackageVersionNormalizer.java @@ -1,17 +1,5 @@ -// Copyright 2000-2017 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; +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.packaging; import com.intellij.openapi.util.text.StringUtil; import one.util.streamex.StreamEx; @@ -29,12 +17,8 @@ import java.util.regex.Pattern; * https://www.python.org/dev/peps/pep-0440/#normalization * and * https://www.python.org/dev/peps/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering. - * - * @deprecated Use {@link com.jetbrains.python.packaging.PyRequirement} instead. - * This class will be removed in 2018.2. */ -@Deprecated -public final class PyRequirementVersionNormalizer { +public final class PyPackageVersionNormalizer { @NotNull private static final String EPOCH_GROUP = "epoch"; @@ -114,10 +98,10 @@ public final class PyRequirementVersionNormalizer { Pattern.CASE_INSENSITIVE); @Nullable - public static PyRequirementVersion normalize(@NotNull String version) { + public static PyPackageVersion normalize(@NotNull String version) { final Matcher matcher = VERSION.matcher(version); if (matcher.matches()) { - return new PyRequirementVersion( + return new PyPackageVersion( normalizeEpoch(matcher), normalizeRelease(matcher), normalizePre(matcher), diff --git a/python/src/com/jetbrains/python/packaging/PyPackagingSettings.java b/python/src/com/jetbrains/python/packaging/PyPackagingSettings.java index 91231116d665..a7c30fe767fc 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackagingSettings.java +++ b/python/src/com/jetbrains/python/packaging/PyPackagingSettings.java @@ -7,8 +7,6 @@ import com.intellij.openapi.components.State; import com.intellij.openapi.project.Project; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.xmlb.XmlSerializerUtil; -import com.jetbrains.python.packaging.requirement.PyRequirementVersion; -import com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer; import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -47,7 +45,7 @@ public class PyPackagingSettings implements PersistentStateComponent { - final PyRequirementVersion normalized = PyRequirementVersionNormalizer.normalize(version); + final PyPackageVersion normalized = PyPackageVersionNormalizer.normalize(version); return normalized == null || normalized.getPre() == null && normalized.getDev() == null; } diff --git a/python/src/com/jetbrains/python/packaging/PyRequirementImpl.kt b/python/src/com/jetbrains/python/packaging/PyRequirementImpl.kt new file mode 100644 index 000000000000..28984ce91670 --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/PyRequirementImpl.kt @@ -0,0 +1,30 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.packaging + +import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec + +/** + * This class is not an API, consider using methods listed below. + * + * @see PyPackageManager.parseRequirement + * @see PyPackageManager.parseRequirements + * + * @see PyRequirementParser.fromText + * @see PyRequirementParser.fromLine + * @see PyRequirementParser.fromFile + */ +data class PyRequirementImpl(private val name: String, + private val versionSpecs: List, + private val installOptions: List, + private val extras: String) : PyRequirement { + + override fun getName() = name + override fun getExtras() = extras + override fun getVersionSpecs() = versionSpecs + override fun getInstallOptions() = installOptions + + override fun match(packages: Collection): PyPackage? { + val normalizedName = name.replace('_', '-') + return packages.firstOrNull { normalizedName.equals(it.name, true) && versionSpecs.all { spec -> spec.matches(it.version) } } + } +} diff --git a/python/src/com/jetbrains/python/packaging/PyRequirementParser.java b/python/src/com/jetbrains/python/packaging/PyRequirementParser.java new file mode 100644 index 000000000000..4b3d728181ba --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/PyRequirementParser.java @@ -0,0 +1,663 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.packaging; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.python.packaging.requirement.PyRequirementRelation; +import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * @see pip install documentation + * @see PEP-508 + * @see PEP-440 + * @see PyRequirement + * @see PyPackageVersionNormalizer + * @see PyPackageManager#parseRequirement(String) + * @see PyPackageManager#parseRequirements(String) + * @see PyPackageManager#parseRequirements(VirtualFile) + */ +public class PyRequirementParser { + + // common regular expressions + + @NotNull + private static final String LINE_WS_REGEXP = "[ \t]"; + + @NotNull + private static final String COMMENT_GROUP = "comment"; + + @NotNull + private static final String COMMENT_REGEXP = "(?<" + COMMENT_GROUP + ">" + LINE_WS_REGEXP + "+#.*)?"; + + @NotNull + private static final String NAME_GROUP = "name"; + + // PEP-508 + // https://www.python.org/dev/peps/pep-0508/ + + @NotNull + private static final String IDENTIFIER_REGEXP = "[A-Za-z0-9]([-_\\.]?[A-Za-z0-9])*"; + + @NotNull + private static final String NAME_REGEXP = "(?<" + NAME_GROUP + ">" + IDENTIFIER_REGEXP + ")"; + + @NotNull + private static final String EXTRAS_REGEXP = + "\\[" + IDENTIFIER_REGEXP + "(" + LINE_WS_REGEXP + "*," + LINE_WS_REGEXP + "*" + IDENTIFIER_REGEXP + ")*" + "\\]"; + + // archive-related regular expressions + + @NotNull + private static final Pattern GITHUB_ARCHIVE_URL = + Pattern.compile("https?://github\\.com/[^/\\s]+/(?<" + NAME_GROUP + ">[^/\\s]+)/archive/\\S+" + COMMENT_REGEXP); + + @NotNull + private static final Pattern GITLAB_ARCHIVE_URL = + Pattern.compile("https?://gitlab\\.com/[^/\\s]+/(?<" + NAME_GROUP + ">[^/\\s]+)/repository/\\S+" + COMMENT_REGEXP); + + @NotNull + private static final Pattern ARCHIVE_URL = + Pattern.compile("https?://\\S+/" + + "(?<" + NAME_GROUP + ">\\S+)" + + "(\\.tar\\.gz|\\.zip)(#(sha1|sha224|sha256|sha384|sha512|md5)=\\w+)?" + COMMENT_REGEXP); + + // vcs-related regular expressions + // don't forget to update calculateVcsInstallOptions(Matcher) after this section changing + + @NotNull + private static final String VCS_EDITABLE_GROUP = "editable"; + + @NotNull + private static final String VCS_EDITABLE_REGEXP = "((?<" + VCS_EDITABLE_GROUP + ">-e|--editable)" + LINE_WS_REGEXP + "+)?"; + + @NotNull + private static final String VCS_SRC_BEFORE_GROUP = "srcb"; + + @NotNull + private static final String VCS_SRC_AFTER_GROUP = "srca"; + + @NotNull + private static final String VCS_SRC_BEFORE_REGEXP = + "(?<" + VCS_SRC_BEFORE_GROUP + ">--src" + LINE_WS_REGEXP + "+\\S+" + LINE_WS_REGEXP + "+)?"; + + @NotNull + private static final String VCS_SRC_AFTER_REGEXP = + "(?<" + VCS_SRC_AFTER_GROUP + ">" + LINE_WS_REGEXP + "+--src" + LINE_WS_REGEXP + "+\\S+)?"; + + @NotNull + private static final String PATH_IN_VCS_GROUP = "path"; + + @NotNull + private static final String PATH_IN_VCS_REGEXP = "(?<" + PATH_IN_VCS_GROUP + ">[^@#\\s]+)"; + + @NotNull + private static final String VCS_REVISION_REGEXP = "(@[^#\\s]+)?"; + + @NotNull + private static final String VCS_EGG_BEFORE_SUBDIR_GROUP = "eggb"; + + @NotNull + private static final String VCS_EGG_AFTER_SUBDIR_GROUP = "egga"; + + @NotNull + private static final String VCS_EXTRAS_BEFORE_SUBDIR_GROUP = "extrasb"; + + @NotNull + private static final String VCS_EXTRAS_AFTER_SUBDIR_GROUP = "extrasa"; + + @NotNull + private static final String VCS_PARAMS_REGEXP = + "(" + + "(" + + "#egg=(?<" + VCS_EGG_BEFORE_SUBDIR_GROUP + ">[^&\\s\\[\\]]+)(?<" + VCS_EXTRAS_BEFORE_SUBDIR_GROUP + ">" + EXTRAS_REGEXP + ")?" + + "(&subdirectory=\\S+)?" + + ")" + + "|" + + "(" + + "#subdirectory=[^&\\s]+" + + "&egg=(?<" + VCS_EGG_AFTER_SUBDIR_GROUP + ">[^\\s\\[\\]]+)(?<" + VCS_EXTRAS_AFTER_SUBDIR_GROUP + ">" + EXTRAS_REGEXP + ")?" + + ")" + + ")?"; + + @NotNull + private static final String VCS_GROUP = "vcs"; + + @NotNull + private static final String VCS_URL_PREFIX = VCS_SRC_BEFORE_REGEXP + VCS_EDITABLE_REGEXP + "(?<" + VCS_GROUP + ">"; + + @NotNull + private static final String VCS_URL_SUFFIX = + PATH_IN_VCS_REGEXP + VCS_REVISION_REGEXP + VCS_PARAMS_REGEXP + ")" + VCS_SRC_AFTER_REGEXP + COMMENT_REGEXP; + + @NotNull + private static final String GIT_USER_AT_REGEXP = "[\\w-]+@"; + + // supports: git+user@... + @NotNull + private static final Pattern GIT_PROJECT_URL = + Pattern.compile(VCS_URL_PREFIX + "git\\+" + GIT_USER_AT_REGEXP + "[^:\\s]+:" + VCS_URL_SUFFIX); + + // supports: bzr+lp:... + @NotNull + private static final Pattern BZR_PROJECT_URL = Pattern.compile(VCS_URL_PREFIX + "bzr\\+lp:" + VCS_URL_SUFFIX); + + // supports: (bzr|git|hg|svn)(+smth)?://... + @NotNull + private static final Pattern VCS_PROJECT_URL = + Pattern.compile(VCS_URL_PREFIX + "(bzr|git|hg|svn)(\\+[A-Za-z]+)?://?[^/]+/" + VCS_URL_SUFFIX); + + // requirement-related regular expressions + // don't forget to update calculateRequirementInstallOptions(Matcher) after this section changing + + @NotNull + private static final String REQUIREMENT_EXTRAS_GROUP = "extras"; + + @NotNull + private static final String REQUIREMENT_EXTRAS_REGEXP = "(?<" + REQUIREMENT_EXTRAS_GROUP + ">" + EXTRAS_REGEXP + ")?"; + + // PEP-440 + // https://www.python.org/dev/peps/pep-0440/ + + @NotNull + private static final String REQUIREMENT_VERSIONS_SPECS_GROUP = "versionspecs"; + + @NotNull + private static final String REQUIREMENT_VERSION_SPEC_REGEXP = "(<=?|!=|===?|>=?|~=)" + LINE_WS_REGEXP + "*[\\.\\*\\+!\\w-]+"; + + @NotNull + private static final String REQUIREMENT_VERSIONS_SPECS_REGEXP = + "(?<" + REQUIREMENT_VERSIONS_SPECS_GROUP + ">" + REQUIREMENT_VERSION_SPEC_REGEXP + + "(" + LINE_WS_REGEXP + "*," + LINE_WS_REGEXP + "*" + REQUIREMENT_VERSION_SPEC_REGEXP + ")*)?"; + + @NotNull + private static final String REQUIREMENT_OPTIONS_GROUP = "options"; + + @NotNull + private static final String REQUIREMENT_OPTIONS_REGEXP = + "(?<" + REQUIREMENT_OPTIONS_GROUP + ">(" + LINE_WS_REGEXP + "+(--global-option|--install-option)=\"[^\"]*\")+)?"; + + @NotNull + private static final String REQUIREMENT_GROUP = "requirement"; + + @NotNull + private static final Pattern REQUIREMENT = Pattern.compile( + "(?<" + REQUIREMENT_GROUP + ">" + + NAME_REGEXP + + LINE_WS_REGEXP + "*" + + REQUIREMENT_EXTRAS_REGEXP + + LINE_WS_REGEXP + "*" + + REQUIREMENT_VERSIONS_SPECS_REGEXP + + ")" + + REQUIREMENT_OPTIONS_REGEXP + + COMMENT_REGEXP); + + @Nullable + public static PyRequirement fromLine(@NotNull String line) { + final PyRequirement githubArchiveUrl = parseGitArchiveUrl(GITHUB_ARCHIVE_URL, line); + if (githubArchiveUrl != null) { + return githubArchiveUrl; + } + + final PyRequirement gitlabArchiveUrl = parseGitArchiveUrl(GITLAB_ARCHIVE_URL, line); + if (gitlabArchiveUrl != null) { + return gitlabArchiveUrl; + } + + final PyRequirement archiveUrl = parseArchiveUrl(line); + if (archiveUrl != null) { + return archiveUrl; + } + + final PyRequirement vcsProjectUrl = parseVcsProjectUrl(line); + if (vcsProjectUrl != null) { + return vcsProjectUrl; + } + + return parseRequirement(line); + } + + @NotNull + public static List fromText(@NotNull String text) { + return fromText(text, null, new HashSet<>()); + } + + @NotNull + public static List fromFile(@NotNull VirtualFile file) { + return fromText(loadText(file), file, new HashSet<>()); + } + + @Nullable + private static PyRequirement parseGitArchiveUrl(@NotNull Pattern pattern, @NotNull String line) { + final Matcher matcher = pattern.matcher(line); + + if (matcher.matches()) { + return new PyRequirementImpl(matcher.group(NAME_GROUP), Collections.emptyList(), + Collections.singletonList(dropComments(line, matcher)), ""); + } + + return null; + } + + @Nullable + private static PyRequirement parseArchiveUrl(@NotNull String line) { + final Matcher matcher = ARCHIVE_URL.matcher(line); + + if (matcher.matches()) { + return createVcsOrArchiveRequirement(parseNameAndVersionFromVcsOrArchive(matcher.group(NAME_GROUP)), + Collections.singletonList(dropComments(line, matcher)), + null); + } + + return null; + } + + @Nullable + private static PyRequirement parseVcsProjectUrl(@NotNull String line) { + final Matcher vcsMatcher = VCS_PROJECT_URL.matcher(line); + if (vcsMatcher.matches()) { + return createVcsRequirement(vcsMatcher); + } + + final Matcher gitMatcher = GIT_PROJECT_URL.matcher(line); + if (gitMatcher.matches()) { + return createVcsRequirement(gitMatcher); + } + + final Matcher bzrMatcher = BZR_PROJECT_URL.matcher(line); + if (bzrMatcher.matches()) { + return createVcsRequirement(bzrMatcher); + } + + return null; + } + + @Nullable + private static PyRequirement parseRequirement(@NotNull String line) { + final Matcher matcher = REQUIREMENT.matcher(line); + if (matcher.matches()) { + final String name = matcher.group(NAME_GROUP); + final List versionSpecs = parseVersionSpecs(matcher.group(REQUIREMENT_VERSIONS_SPECS_GROUP)); + final List installOptions = calculateRequirementInstallOptions(matcher); + final String extras = matcher.group(REQUIREMENT_EXTRAS_GROUP); + + if (extras == null) { + return new PyRequirementImpl(name, versionSpecs, installOptions, ""); + } + else { + return new PyRequirementImpl(name, versionSpecs, installOptions, extras); + } + } + + return null; + } + + @NotNull + private static List fromText(@NotNull String text, + @Nullable VirtualFile containingFile, + @NotNull Set visitedFiles) { + if (containingFile != null) { + visitedFiles.add(containingFile); + } + + return StreamEx + .of(splitByLinesAndCollapse(text)) + .flatCollection(line -> parseLine(line, containingFile, visitedFiles)) + .nonNull() + .distinct() + .toList(); + } + + @NotNull + private static String loadText(@NotNull VirtualFile file) { + final Document document = FileDocumentManager.getInstance().getDocument(file); + + return document == null ? "" : document.getText(); + } + + @NotNull + private static String dropComments(@NotNull String line, @NotNull Matcher matcher) { + final int commentIndex = matcher.start(COMMENT_GROUP); + + if (commentIndex == -1) { + return line; + } + + return line.substring(0, findFirstNotWhiteSpaceBefore(line, commentIndex) + 1); + } + + @NotNull + private static Pair parseNameAndVersionFromVcsOrArchive(@NotNull String name) { + boolean isName = true; + final List nameParts = new ArrayList<>(); + final List versionParts = new ArrayList<>(); + + for (String part : StringUtil.split(name, "-")) { + final boolean partStartsWithDigit = !part.isEmpty() && Character.isDigit(part.charAt(0)); + + if (partStartsWithDigit || "dev".equals(part)) { + isName = false; + } + + if (isName) { + nameParts.add(part); + } + else { + versionParts.add(part); + } + } + + return Pair.create(normalizeVcsOrArchiveNameParts(nameParts), normalizeVcsOrArchiveVersionParts(versionParts)); + } + + @NotNull + private static PyRequirement createVcsOrArchiveRequirement(@NotNull Pair nameAndVersion, + @NotNull List installOptions, + @Nullable String extras) { + final String name = nameAndVersion.getFirst(); + final String version = nameAndVersion.getSecond(); + + if (version == null) { + if (extras == null) { + return new PyRequirementImpl(name, Collections.emptyList(), installOptions, ""); + } + else { + return new PyRequirementImpl(name, Collections.emptyList(), installOptions, extras); + } + } + + final List versionSpecs = Collections.singletonList(PyRequirementsKt.pyRequirementVersionSpec( + PyRequirementRelation.EQ, version)); + if (extras == null) { + return new PyRequirementImpl(name, versionSpecs, installOptions, ""); + } + else { + return new PyRequirementImpl(name, versionSpecs, installOptions, extras); + } + } + + @NotNull + private static PyRequirement createVcsRequirement(@NotNull Matcher matcher) { + final String path = matcher.group(PATH_IN_VCS_GROUP); + final String egg = getEgg(matcher); + + final String project = extractProject(dropTrunk(dropRevision(path))); + final Pair nameAndVersion = + parseNameAndVersionFromVcsOrArchive(egg == null ? StringUtil.trimEnd(project, ".git") : egg); + + return createVcsOrArchiveRequirement(nameAndVersion, calculateVcsInstallOptions(matcher), getVcsExtras(matcher)); + } + + @NotNull + private static List parseVersionSpecs(@Nullable String versionSpecs) { + if (versionSpecs == null) return Collections.emptyList(); + + return StreamSupport + .stream(StringUtil.tokenize(versionSpecs, ",").spliterator(), false) + .map(String::trim) + .map(PyRequirementParser::parseVersionSpec) + .filter(req -> req != null) + .collect(Collectors.toList()); + } + + @NotNull + private static List calculateRequirementInstallOptions(@NotNull Matcher matcher) { + final List result = new ArrayList<>(); + result.add(matcher.group(REQUIREMENT_GROUP)); + + final String requirementOptions = matcher.group(REQUIREMENT_OPTIONS_GROUP); + if (requirementOptions != null) { + boolean isKey = true; + for (String token : StringUtil.tokenize(requirementOptions, "\"")) { + result.add(isKey ? token.substring(findFirstNotWhiteSpaceAfter(token, 0), token.length() - 1) : token); + isKey = !isKey; + } + } + + return result; + } + + @NotNull + private static List splitByLinesAndCollapse(@NotNull String text) { + final List result = new ArrayList<>(); + final StringBuilder sb = new StringBuilder(); + + for (String line : StringUtil.splitByLines(text)) { + if (line.endsWith("\\") && !line.endsWith("\\\\")) { + sb.append(line, 0, line.length() - 1); + } + else { + if (sb.length() == 0) { + result.add(line); + } + else { + sb.append(line); + + result.add(sb.toString()); + + sb.setLength(0); + } + } + } + + return result; + } + + @NotNull + private static List parseLine(@NotNull String line, + @Nullable VirtualFile containingFile, + @NotNull Set visitedFiles) { + if (line.startsWith("-r")) { + return parseRecursiveLine(line, containingFile, visitedFiles, "-r".length()); + } + + if (line.startsWith("--requirement ")) { + return parseRecursiveLine(line, containingFile, visitedFiles, "--requirement ".length()); + } + + return Collections.singletonList(fromLine(line)); + } + + @NotNull + private static String normalizeVcsOrArchiveNameParts(@NotNull List nameParts) { + return normalizeName(StringUtil.join(nameParts, "-")); + } + + @Nullable + private static String normalizeVcsOrArchiveVersionParts(@NotNull List versionParts) { + return versionParts.isEmpty() ? null : normalizeVersion(StringUtil.join(versionParts, "-")); + } + + @NotNull + private static List calculateVcsInstallOptions(@NotNull Matcher matcher) { + final List result = new ArrayList<>(); + + final String srcBefore = matcher.group(VCS_SRC_BEFORE_GROUP); + if (srcBefore != null) { + result.addAll(Arrays.asList(srcBefore.split("\\s+"))); + } + + final String editable = matcher.group(VCS_EDITABLE_GROUP); + if (editable != null) { + result.add(editable); + } + + result.add(matcher.group(VCS_GROUP)); + + final String srcAfter = matcher.group(VCS_SRC_AFTER_GROUP); + if (srcAfter != null) { + result.addAll(Arrays.asList(srcAfter.split("\\s+")).subList(1, 3)); // skip spaces before --src and get only two values + } + + return result; + } + + @Nullable + private static String getEgg(@NotNull Matcher matcher) { + final String beforeSubdir = matcher.group(VCS_EGG_BEFORE_SUBDIR_GROUP); + + return beforeSubdir == null ? matcher.group(VCS_EGG_AFTER_SUBDIR_GROUP) : beforeSubdir; + } + + @NotNull + private static String extractProject(@NotNull String path) { + final int end = path.endsWith("/") ? path.length() - 1 : path.length(); + final int slashIndex = path.lastIndexOf("/", end - 1); + + if (slashIndex != -1) { + return path.substring(slashIndex + 1, end); + } + + if (end != path.length()) { + return path.substring(0, end); + } + + return path; + } + + @NotNull + private static String dropTrunk(@NotNull String path) { + final String slashTrunk = "/trunk"; + + if (path.endsWith(slashTrunk)) { + return path.substring(0, path.length() - slashTrunk.length()); + } + + final String slashTrunkSlash = "/trunk/"; + + if (path.endsWith(slashTrunkSlash)) { + return path.substring(0, path.length() - slashTrunkSlash.length()); + } + + return path; + } + + @NotNull + private static String dropRevision(@NotNull String path) { + final int atIndex = path.lastIndexOf("@"); + + if (atIndex != -1) { + return path.substring(0, atIndex); + } + + return path; + } + + @Nullable + private static String getVcsExtras(@NotNull Matcher matcher) { + final String beforeSubdir = matcher.group(VCS_EXTRAS_BEFORE_SUBDIR_GROUP); + + return beforeSubdir == null ? matcher.group(VCS_EXTRAS_AFTER_SUBDIR_GROUP) : beforeSubdir; + } + + @Nullable + private static PyRequirementVersionSpec parseVersionSpec(@NotNull String versionSpec) { + PyRequirementRelation relation = null; + + if (versionSpec.startsWith("===")) { + relation = PyRequirementRelation.STR_EQ; + } + else if (versionSpec.startsWith("==")) { + relation = PyRequirementRelation.EQ; + } + else if (versionSpec.startsWith("<=")) { + relation = PyRequirementRelation.LTE; + } + else if (versionSpec.startsWith(">=")) { + relation = PyRequirementRelation.GTE; + } + else if (versionSpec.startsWith("<")) { + relation = PyRequirementRelation.LT; + } + else if (versionSpec.startsWith(">")) { + relation = PyRequirementRelation.GT; + } + else if (versionSpec.startsWith("~=")) { + relation = PyRequirementRelation.COMPATIBLE; + } + else if (versionSpec.startsWith("!=")) { + relation = PyRequirementRelation.NE; + } + + if (relation != null) { + final int versionIndex = findFirstNotWhiteSpaceAfter(versionSpec, relation.getPresentableText().length()); + return PyRequirementsKt.pyRequirementVersionSpec(relation, versionSpec.substring(versionIndex)); + } + + return null; + } + + @NotNull + private static List parseRecursiveLine(@NotNull String line, + @Nullable VirtualFile containingFile, + @NotNull Set visitedFiles, + int flagLength) { + if (containingFile == null) return Collections.emptyList(); + + final int pathIndex = findFirstNotWhiteSpaceAfter(line, flagLength); + if (pathIndex == line.length()) return Collections.emptyList(); + + final String path = FileUtil.toSystemIndependentName(line.substring(pathIndex)); + final VirtualFile file = findRecursiveFile(containingFile, path); + + if (file != null && !visitedFiles.contains(file)) { + return fromText(loadText(file), file, visitedFiles); + } + + return Collections.emptyList(); + } + + @NotNull + private static String normalizeName(@NotNull String s) { + return s.replace('_', '-'); + } + + @NotNull + private static String normalizeVersion(@NotNull String s) { + return s.replace('_', '-').replaceAll("-?py[\\d\\.]+", ""); + } + + private static int findFirstNotWhiteSpaceAfter(@NotNull String line, int beginIndex) { + for (int i = beginIndex; i < line.length(); i++) { + if (!StringUtil.isWhiteSpace(line.charAt(i))) { + return i; + } + } + + return line.length(); + } + + private static int findFirstNotWhiteSpaceBefore(@NotNull String line, int beginIndex) { + for (int i = beginIndex; i >= 0; i--) { + if (!StringUtil.isWhiteSpace(line.charAt(i))) { + return i; + } + } + + return -1; + } + + @Nullable + private static VirtualFile findRecursiveFile(@NotNull VirtualFile containingFile, @NotNull String path) { + final VirtualFile dir = containingFile.getParent(); + if (dir == null) return null; + + final VirtualFile file = dir.findFileByRelativePath(path); + if (file != null) return file; + + return LocalFileSystem.getInstance().findFileByPath(path); + } +} diff --git a/python/src/com/jetbrains/python/packaging/PyRequirements.kt b/python/src/com/jetbrains/python/packaging/PyRequirements.kt new file mode 100644 index 000000000000..811f476007ef --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/PyRequirements.kt @@ -0,0 +1,126 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.packaging + +import com.intellij.openapi.util.text.StringUtil +import com.jetbrains.python.packaging.requirement.PyRequirementRelation +import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec + +/** + * This helper is not an API, consider using methods listed below. + * + * @see PyPackageManager.parseRequirement + * @see PyPackageManager.parseRequirements + * + * @see PyRequirementParser.fromLine + * @see PyRequirementParser.fromText + * @see PyRequirementParser.fromFile + */ +fun pyRequirement(name: String): PyRequirement = PyRequirementImpl(name, emptyList(), listOf(name), "") + +/** + * This helper is not an API, consider using methods listed below. + * If given version could not be normalized, then specified relation will be replaced with [PyRequirementRelation.STR_EQ]. + * + * @see PyPackageManager.parseRequirement + * @see PyPackageManager.parseRequirements + * + * @see PyRequirementParser.fromLine + * @see PyRequirementParser.fromText + * @see PyRequirementParser.fromFile + * + * @see pyRequirementVersionSpec + */ +fun pyRequirement(name: String, relation: PyRequirementRelation, version: String): PyRequirement { + val versionSpec = pyRequirementVersionSpec(relation, version) + return PyRequirementImpl(name, listOf(versionSpec), listOf(name + relation.presentableText + version), "") +} + +/** + * This method could be used to obtain [PyRequirementVersionSpec] instances with specified relation and version. + * If given version could not be normalized, then specified relation will be replaced with [PyRequirementRelation.STR_EQ]. + * + * @see PyPackageVersionNormalizer.normalize + */ +fun pyRequirementVersionSpec(relation: PyRequirementRelation, version: String): PyRequirementVersionSpec { + if (relation == PyRequirementRelation.STR_EQ) return PyRequirementVersionSpecImpl(relation, null, version) + + return PyPackageVersionNormalizer + .normalize(version) + .let { + if (it == null) PyRequirementVersionSpecImpl(PyRequirementRelation.STR_EQ, null, version) else pyRequirementVersionSpec(relation, it) + } +} + +/** + * This method could be used to obtain [PyRequirementVersionSpec] instances with specified relation and version. + * + * @see PyPackageVersion + * @see PyPackageVersionNormalizer.normalize + */ +fun pyRequirementVersionSpec(relation: PyRequirementRelation, version: PyPackageVersion): PyRequirementVersionSpec { + return PyRequirementVersionSpecImpl(relation, version, version.presentableText) +} + +/** + * Instances of this class MUST be obtained from [pyRequirementVersionSpec]. + */ +private data class PyRequirementVersionSpecImpl(private val relation: PyRequirementRelation, + private val parsedVersion: PyPackageVersion?, + private val version: String) : PyRequirementVersionSpec { + + override fun getRelation() = relation + override fun getVersion() = version + + override fun matches(version: String): Boolean { + val comparator = PyPackageVersionComparator.STR_COMPARATOR + + return when (relation) { + PyRequirementRelation.LT -> comparator.compare(version, this.version) < 0 + PyRequirementRelation.LTE -> comparator.compare(version, this.version) <= 0 + PyRequirementRelation.GT -> comparator.compare(version, this.version) > 0 + PyRequirementRelation.GTE -> comparator.compare(version, this.version) >= 0 + PyRequirementRelation.EQ -> { + parsedVersion!! + + val publicAndLocalVersions = splitIntoPublicAndLocalVersions(parsedVersion) + val otherPublicAndLocalVersions = splitIntoPublicAndLocalVersions(version) + val publicVersionsAreSame = comparator.compare(otherPublicAndLocalVersions.first, publicAndLocalVersions.first) == 0 + + return publicVersionsAreSame && + (publicAndLocalVersions.second.isEmpty() || otherPublicAndLocalVersions.second == publicAndLocalVersions.second) + } + PyRequirementRelation.NE -> comparator.compare(version, this.version) != 0 + PyRequirementRelation.COMPATIBLE -> { + parsedVersion!! + + return pyRequirementVersionSpec(PyRequirementRelation.GTE, + parsedVersion).matches(version) && + pyRequirementVersionSpec(PyRequirementRelation.EQ, + toEqPartOfCompatibleRelation(parsedVersion)).matches(version) + } + PyRequirementRelation.STR_EQ -> version == this.version + else -> false + } + } + + private fun splitIntoPublicAndLocalVersions(version: PyPackageVersion): Pair { + return version.copy(local = null).presentableText to StringUtil.notNullize(version.local) + } + + private fun splitIntoPublicAndLocalVersions(version: String): Pair { + val publicAndLocalVersions = version.split('+', limit = 2) + + val publicVersion = publicAndLocalVersions[0] + val localVersion = if (publicAndLocalVersions.size == 1) "" else publicAndLocalVersions[1] + + return publicVersion to localVersion + } + + private fun toEqPartOfCompatibleRelation(version: PyPackageVersion): PyPackageVersion { + val release = version.release + val lastPoint = release.lastIndexOf('.') + + return if (lastPoint == -1) version + else PyPackageVersion(version.epoch, release.substring(0, lastPoint + 1) + "*", null, null, null, null) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/ui/PyChooseRequirementsDialog.java b/python/src/com/jetbrains/python/packaging/ui/PyChooseRequirementsDialog.java index 9959e8a75071..38241d92115c 100644 --- a/python/src/com/jetbrains/python/packaging/ui/PyChooseRequirementsDialog.java +++ b/python/src/com/jetbrains/python/packaging/ui/PyChooseRequirementsDialog.java @@ -39,7 +39,7 @@ public class PyChooseRequirementsDialog extends DialogWrapper { myRequirementsChooser = new ElementsChooser(true) { @Override public String getItemText(@NotNull PyRequirement requirement) { - return requirement.toString(); + return requirement.getPresentableText(); } }; myRequirementsChooser.setElements(requirements, true); diff --git a/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java b/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java index 84dbdd5dcb16..f843a7b77d63 100644 --- a/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java +++ b/python/src/com/jetbrains/python/packaging/ui/PyPackageManagementService.java @@ -17,6 +17,7 @@ import com.intellij.webcore.packaging.PackageManagementServiceEx; import com.intellij.webcore.packaging.RepoPackage; import com.jetbrains.python.packaging.*; import com.jetbrains.python.packaging.PyPIPackageUtil.PackageDetails; +import com.jetbrains.python.packaging.requirement.PyRequirementRelation; import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.sdk.PySdkUtil; import com.jetbrains.python.sdk.PythonSdkType; @@ -187,13 +188,9 @@ public class PyPackageManagementService extends PackageManagementServiceEx { if (forceUpgrade) { extraArgs.add("-U"); } - final PyRequirement req; - if (version != null) { - req = new PyRequirement(packageName, version); - } - else { - req = new PyRequirement(packageName); - } + final PyRequirement req = version == null + ? PyRequirementsKt.pyRequirement(packageName) + : PyRequirementsKt.pyRequirement(packageName, PyRequirementRelation.EQ, version); final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, mySdk, new PyPackageManagerUI.Listener() { @Override diff --git a/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java b/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java index 82c7bd85b67f..f3783deb767a 100644 --- a/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java +++ b/python/testSrc/com/jetbrains/env/python/PyPackagingTest.java @@ -12,7 +12,7 @@ import com.jetbrains.env.PyTestTask; import com.jetbrains.env.Staging; import com.jetbrains.python.packaging.PyPackage; import com.jetbrains.python.packaging.PyPackageManager; -import com.jetbrains.python.packaging.PyRequirement; +import com.jetbrains.python.packaging.requirement.PyRequirementRelation; import com.jetbrains.python.sdk.PythonSdkType; import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor; @@ -30,6 +30,7 @@ import java.util.List; import java.util.Set; import static com.intellij.testFramework.UsefulTestCase.assertInstanceOf; +import static com.jetbrains.python.packaging.PyRequirementsKt.pyRequirement; import static org.junit.Assert.*; /** @@ -116,8 +117,8 @@ public class PyPackagingTest extends PyEnvTestCase { final PyPackageManager manager = PyPackageManager.getInstance(venvSdk); final List packages1 = manager.refreshAndGetPackages(false); // TODO: Install Markdown from a local file - manager.install(list(PyRequirement.fromLine("Markdown<2.2"), - new PyRequirement("httplib2")), Collections.emptyList()); + manager.install(Arrays.asList(pyRequirement("Markdown", PyRequirementRelation.LT, "2.2"), pyRequirement("httplib2")), + Collections.emptyList()); final List packages2 = manager.refreshAndGetPackages(false); final PyPackage markdown2 = findPackage("Markdown", packages2); assertNotNull(markdown2); @@ -125,7 +126,7 @@ public class PyPackagingTest extends PyEnvTestCase { final PyPackage pip1 = findPackage("pip", packages1); assertNotNull(pip1); assertEquals("pip", pip1.getName()); - manager.uninstall(list(pip1)); + manager.uninstall(Collections.singletonList(pip1)); final List packages3 = manager.refreshAndGetPackages(false); final PyPackage pip2 = findPackage("pip", packages3); assertNull(pip2); @@ -150,11 +151,6 @@ public class PyPackagingTest extends PyEnvTestCase { return null; } - private static List list(T... xs) { - return Arrays.asList(xs); - } - - private abstract static class PyPackagingTestTask extends PyExecutionFixtureTestTask { PyPackagingTestTask() { super(null); diff --git a/python/testSrc/com/jetbrains/python/packaging/PyPackageTest.java b/python/testSrc/com/jetbrains/python/packaging/PyPackageTest.java index 68055d1cc028..3fe255a78ade 100644 --- a/python/testSrc/com/jetbrains/python/packaging/PyPackageTest.java +++ b/python/testSrc/com/jetbrains/python/packaging/PyPackageTest.java @@ -40,7 +40,7 @@ public class PyPackageTest extends TestCase { @NotNull private static PyRequirement createRequirement(@NotNull String options) { - final PyRequirement requirement = PyPackageUtil.fix(PyRequirement.fromLine(options)); + final PyRequirement requirement = PyRequirementParser.fromLine(options); assertNotNull(requirement); return requirement; } diff --git a/python/testSrc/com/jetbrains/python/packaging/PyPackageUtilTest.java b/python/testSrc/com/jetbrains/python/packaging/PyPackageUtilTest.java index 25793d43017e..6ebbcca6b1f6 100644 --- a/python/testSrc/com/jetbrains/python/packaging/PyPackageUtilTest.java +++ b/python/testSrc/com/jetbrains/python/packaging/PyPackageUtilTest.java @@ -55,7 +55,7 @@ public class PyPackageUtilTest extends PyTestCase { // PY-18966 public void testSetupPyDependencyLinksReading() { final List actual = PyPackageUtil.findSetupPyRequires(myFixture.getModule()); - final List expected = PyRequirement.fromText( + final List expected = PyRequirementParser.fromText( "sqlalchemy >=1.0.12, <1.1\ngit+https://github.com/mysql/mysql-connector-python.git@2.1.3#egg=mysql-connector-python-2.1.3"); assertEquals(expected, actual); @@ -183,7 +183,7 @@ public class PyPackageUtilTest extends PyTestCase { } private static void checkRequirements(@Nullable List actual, int fromIndex) { - final List expected = PyRequirement.fromText("Markdown\nNewDjango==1.3.1\nnumpy\nmynose"); + final List expected = PyRequirementParser.fromText("Markdown\nNewDjango==1.3.1\nnumpy\nmynose"); assertEquals(expected.subList(fromIndex, expected.size()), actual); } @@ -205,7 +205,7 @@ public class PyPackageUtilTest extends PyTestCase { checkSetupArgumentText(module, keyword, "['NewDjango==1.3.1', 'Markdown']"); final List actual = PyPackageUtil.findSetupPyRequires(module); - final List expected = PyRequirement.fromText("NewDjango==1.3.1\nMarkdown\nnumpy\nmynose"); + final List expected = PyRequirementParser.fromText("NewDjango==1.3.1\nMarkdown\nnumpy\nmynose"); assertEquals(expected, actual); } diff --git a/python/testSrc/com/jetbrains/python/packaging/PyPackageVersionComparatorTest.kt b/python/testSrc/com/jetbrains/python/packaging/PyPackageVersionComparatorTest.kt index 87c57d4d2fce..26cc10868bef 100644 --- a/python/testSrc/com/jetbrains/python/packaging/PyPackageVersionComparatorTest.kt +++ b/python/testSrc/com/jetbrains/python/packaging/PyPackageVersionComparatorTest.kt @@ -2,60 +2,58 @@ package com.jetbrains.python.packaging import com.jetbrains.python.fixtures.PyTestCase -import com.jetbrains.python.packaging.requirement.PyRequirementVersion -import com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer import one.util.streamex.StreamEx class PyPackageVersionComparatorTest : PyTestCase() { fun testEpoch() { - check(PyPackageVersion("0", "1.2.3"), - PyPackageVersion("0", "1.2.3"), - PyPackageVersion("1", "1.2.3")) + check(normalize("0!1.2.3"), + normalize("0!1.2.3"), + normalize("1!1.2.3")) } fun testSameLengthRelease() { - check(PyPackageVersion(release = "1.2"), - PyPackageVersion(release = "1.2"), - PyPackageVersion(release = "1.3")) + check(normalize("1.2"), + normalize("1.2"), + normalize("1.3")) - check(PyPackageVersion(release = "1.2.3"), - PyPackageVersion(release = "1.2.3"), - PyPackageVersion(release = "1.2.4")) + check(normalize("1.2.3"), + normalize("1.2.3"), + normalize("1.2.4")) } fun testDifferentLengthRelease() { - check(PyPackageVersion(release = "1.2"), - PyPackageVersion(release = "1.2.0"), - PyPackageVersion(release = "1.2.1")) + check(normalize("1.2"), + normalize("1.2.0"), + normalize("1.2.1")) - check(PyPackageVersion(release = "1.2.3"), - PyPackageVersion(release = "1.2.3.0"), - PyPackageVersion(release = "1.2.3.1")) + check(normalize("1.2.3"), + normalize("1.2.3.0"), + normalize("1.2.3.1")) } fun testPost() { - check(PyPackageVersion(release = "1.2", post = "post1"), - PyPackageVersion(release = "1.2", post = "post1"), - PyPackageVersion(release = "1.2", post = "post2")) + check(normalize("1.2.post1"), + normalize("1.2.post1"), + normalize("1.2.post2")) } fun testPre() { - check(PyPackageVersion(release = "1.2", pre = "a1"), - PyPackageVersion(release = "1.2", pre = "a1"), - PyPackageVersion(release = "1.2", pre = "a2")) + check(normalize("1.2a1"), + normalize("1.2a1"), + normalize("1.2a2")) } fun testDev() { - check(PyPackageVersion(release = "1.2", dev = "dev1"), - PyPackageVersion(release = "1.2", dev = "dev1"), - PyPackageVersion(release = "1.2", dev = "dev2")) + check(normalize("1.2.dev1"), + normalize("1.2.dev1"), + normalize("1.2.dev2")) } fun testLocal() { - check(PyPackageVersion(release = "1.2", local = "abc"), - PyPackageVersion(release = "1.2", local = "abc"), - PyPackageVersion(release = "1.2", local = "def")) + check(normalize("1.2+abc"), + normalize("1.2+abc"), + normalize("1.2+def")) } fun testSameReleaseOrder() { @@ -71,7 +69,7 @@ class PyPackageVersionComparatorTest : PyTestCase() { normalize("1.0.c1.dev1"), normalize("1.0.c1"), normalize("1.0.c1.post1"), - PyPackageVersion(release = "1.0"), + normalize("1.0"), normalize("1.0.post1.dev1"), normalize("1.0.post1") ) @@ -91,7 +89,7 @@ class PyPackageVersionComparatorTest : PyTestCase() { normalize("1.0.c1.dev1"), normalize("1.0.c1"), normalize("1.0.c1.post1"), - PyPackageVersion(release = "1.0"), + normalize("1.0"), normalize("1.0.post1.dev1"), normalize("1.0.post1"), @@ -105,7 +103,7 @@ class PyPackageVersionComparatorTest : PyTestCase() { normalize("2.0.c1.dev1"), normalize("2.0.c1"), normalize("2.0.c1.post1"), - PyPackageVersion(release = "2.0"), + normalize("2.0"), normalize("2.0.post1.dev1"), normalize("2.0.post1") ) @@ -113,7 +111,7 @@ class PyPackageVersionComparatorTest : PyTestCase() { } fun testCompatible() { - val pkg = PyPackageVersion(release = "1.*") + val pkg = normalize("1.*") listOf( normalize("1.0.dev1"), @@ -126,16 +124,14 @@ class PyPackageVersionComparatorTest : PyTestCase() { normalize("1.0.c1.dev1"), normalize("1.0.c1"), normalize("1.0.c1.post1"), - PyPackageVersion(release = "1.0"), + normalize("1.0"), normalize("1.0.post1.dev1"), normalize("1.0.post1") ) .forEach { check(it, pkg, true) } } - - private fun normalize(version: String) = PyRequirementVersionNormalizer.normalize(version)!!.toPkgVersion() - private fun PyRequirementVersion.toPkgVersion() = PyPackageVersion(epoch, release, pre, post, dev, local) + private fun normalize(version: String) = PyPackageVersionNormalizer.normalize(version)!! private fun check(less: PyPackageVersion, equal: PyPackageVersion, greater: PyPackageVersion) { check(less, equal, true) @@ -155,17 +151,17 @@ class PyPackageVersionComparatorTest : PyTestCase() { assertFalse(message, pkg1 === pkg2) assertTrue(message, PyPackageVersionComparator.compare(pkg1, pkg2) == 0) - assertTrue(message, PyPackageVersionComparator.STR_COMPARATOR.compare(pkg1.toString(), pkg2.toString()) == 0) + assertTrue(message, PyPackageVersionComparator.STR_COMPARATOR.compare(pkg1.presentableText, pkg2.presentableText) == 0) assertTrue(message, PyPackageVersionComparator.compare(pkg2, pkg1) == 0) - assertTrue(message, PyPackageVersionComparator.STR_COMPARATOR.compare(pkg2.toString(), pkg1.toString()) == 0) + assertTrue(message, PyPackageVersionComparator.STR_COMPARATOR.compare(pkg2.presentableText, pkg1.presentableText) == 0) } else { assertTrue(message, PyPackageVersionComparator.compare(pkg1, pkg2) < 0) - assertTrue(message, PyPackageVersionComparator.STR_COMPARATOR.compare(pkg1.toString(), pkg2.toString()) < 0) + assertTrue(message, PyPackageVersionComparator.STR_COMPARATOR.compare(pkg1.presentableText, pkg2.presentableText) < 0) assertTrue(message, PyPackageVersionComparator.compare(pkg2, pkg1) > 0) - assertTrue(message, PyPackageVersionComparator.STR_COMPARATOR.compare(pkg2.toString(), pkg1.toString()) > 0) + assertTrue(message, PyPackageVersionComparator.STR_COMPARATOR.compare(pkg2.presentableText, pkg1.presentableText) > 0) } } } \ 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 2bb7b60b63e2..b0430f297b77 100644 --- a/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java +++ b/python/testSrc/com/jetbrains/python/packaging/PyRequirementTest.java @@ -7,17 +7,20 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.fixtures.PyTestCase; import com.jetbrains.python.packaging.requirement.PyRequirementRelation; -import com.jetbrains.python.packaging.requirement.PyRequirementVersion; import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import static com.jetbrains.python.packaging.PyPackageUtil.fix; +import static com.jetbrains.python.packaging.PyRequirementParser.fromLine; +import static com.jetbrains.python.packaging.PyRequirementsKt.pyRequirement; +import static com.jetbrains.python.packaging.PyRequirementsKt.pyRequirementVersionSpec; +import static com.jetbrains.python.packaging.requirement.PyRequirementRelation.*; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; /** * @author vlan @@ -1901,50 +1904,37 @@ public class PyRequirementTest extends PyTestCase { public void testMinusInRequirementEggName() { final String line = "git://github.com/toastdriven/django-haystack.git#egg=django-haystack"; - assertEquals(new PyRequirement("django-haystack", Collections.emptyList(), Collections.singletonList(line)), - PyRequirement.fromLine(line)); + assertEquals(new PyRequirementImpl("django-haystack", emptyList(), singletonList(line), ""), fromLine(line)); } public void testDevInRequirementEggName() { doTest("django-haystack", "dev", "git://github.com/toastdriven/django-haystack.git#egg=django_haystack-dev"); - doTest("django-haystack", "dev", "git://github.com/toastdriven/django-haystack.git#egg=django-haystack-dev"); + doTest("django-haystack", "dev", "git://github.com/toastdriven/django-haystack.git#egg=django_haystack-dev"); } // PY-26844 public void testExtrasInRequirementEggName() { final String line1 = "git://github.com/python-social-auth/social-core.git#egg=social-auth-core[openidconnect]"; - assertEquals(new PyRequirement("social-auth-core", Collections.emptyList(), Collections.singletonList(line1), "[openidconnect]"), - PyRequirement.fromLine(line1)); + assertEquals(new PyRequirementImpl("social-auth-core", emptyList(), singletonList(line1), "[openidconnect]"), fromLine(line1)); final String line2 = "git://github.com/python-social-auth/social-core.git#egg=social-auth-core[openidconnect,security]"; - assertEquals( - new PyRequirement("social-auth-core", Collections.emptyList(), Collections.singletonList(line2), "[openidconnect,security]"), - PyRequirement.fromLine(line2) - ); + assertEquals(new PyRequirementImpl("social-auth-core", emptyList(), singletonList(line2), "[openidconnect,security]"), fromLine(line2)); final String line3 = "git://github.com/python-social-auth/social-core.git#egg=social-auth-core[openidconnect]&subdirectory=clients/python"; - assertEquals(new PyRequirement("social-auth-core", Collections.emptyList(), Collections.singletonList(line3), "[openidconnect]"), - PyRequirement.fromLine(line3)); + assertEquals(new PyRequirementImpl("social-auth-core", emptyList(), singletonList(line3), "[openidconnect]"), fromLine(line3)); final String line4 = "git://github.com/python-social-auth/social-core.git#egg=social-auth-core[openidconnect,security]&subdirectory=clients/python"; - assertEquals( - new PyRequirement("social-auth-core", Collections.emptyList(), Collections.singletonList(line4), "[openidconnect,security]"), - PyRequirement.fromLine(line4) - ); + assertEquals(new PyRequirementImpl("social-auth-core", emptyList(), singletonList(line4), "[openidconnect,security]"), fromLine(line4)); final String line5 = "git://github.com/python-social-auth/social-core.git#subdirectory=clients/python&egg=social-auth-core[openidconnect]"; - assertEquals(new PyRequirement("social-auth-core", Collections.emptyList(), Collections.singletonList(line5), "[openidconnect]"), - PyRequirement.fromLine(line5)); + assertEquals(new PyRequirementImpl("social-auth-core", emptyList(), singletonList(line5), "[openidconnect]"), fromLine(line5)); final String line6 = "git://github.com/python-social-auth/social-core.git#subdirectory=clients/python&egg=social-auth-core[openidconnect,security]"; - assertEquals( - new PyRequirement("social-auth-core", Collections.emptyList(), Collections.singletonList(line6), "[openidconnect,security]"), - PyRequirement.fromLine(line6) - ); + assertEquals(new PyRequirementImpl("social-auth-core", emptyList(), singletonList(line6), "[openidconnect,security]"), fromLine(line6)); } // LOCAL DIR @@ -1958,29 +1948,28 @@ public class PyRequirementTest extends PyTestCase { // TODO: hashes // https://www.python.org/dev/peps/pep-0508/#names public void testRequirement() { - assertEquals(new PyRequirement("Orange-Bioinformatics"), PyRequirement.fromLine("Orange-Bioinformatics")); - assertEquals(new PyRequirement("MOCPy"), PyRequirement.fromLine("MOCPy")); - assertEquals(new PyRequirement("score.webassets"), PyRequirement.fromLine("score.webassets")); - assertEquals(new PyRequirement("pip_helpers"), PyRequirement.fromLine("pip_helpers")); - assertEquals(new PyRequirement("Django"), PyRequirement.fromLine("Django")); - assertEquals(new PyRequirement("django"), PyRequirement.fromLine("django")); - assertEquals(new PyRequirement("pinax-utils"), PyRequirement.fromLine("pinax-utils")); - assertEquals(new PyRequirement("no_limit_nester"), PyRequirement.fromLine("no_limit_nester")); - assertEquals(new PyRequirement("Flask-Celery-py3"), PyRequirement.fromLine("Flask-Celery-py3")); + assertEquals(pyRequirement("Orange-Bioinformatics"), fromLine("Orange-Bioinformatics")); + assertEquals(pyRequirement("MOCPy"), fromLine("MOCPy")); + assertEquals(pyRequirement("score.webassets"), fromLine("score.webassets")); + assertEquals(pyRequirement("pip_helpers"), fromLine("pip_helpers")); + assertEquals(pyRequirement("Django"), fromLine("Django")); + assertEquals(pyRequirement("django"), fromLine("django")); + assertEquals(pyRequirement("pinax-utils"), fromLine("pinax-utils")); + assertEquals(pyRequirement("no_limit_nester"), fromLine("no_limit_nester")); + assertEquals(pyRequirement("Flask-Celery-py3"), fromLine("Flask-Celery-py3")); } // https://www.python.org/dev/peps/pep-0440/ public void testRequirementVersion() { - assertEquals(new PyRequirement("Orange-Bioinformatics", "2.5a20"), PyRequirement.fromLine("Orange-Bioinformatics==2.5a20")); - assertEquals(new PyRequirement("MOCPy", "0.1.0.dev0"), PyRequirement.fromLine("MOCPy==0.1.0.dev0")); - assertEquals(new PyRequirement("score.webassets", "0.2.3"), PyRequirement.fromLine("score.webassets==0.2.3")); - assertEquals(new PyRequirement("pip_helpers", "0.5.post6"), PyRequirement.fromLine("pip_helpers==0.5.post6")); - assertEquals(new PyRequirement("Django", "1.9rc1"), PyRequirement.fromLine("Django==1.9rc1")); - assertEquals(new PyRequirement("django", "1!1"), PyRequirement.fromLine("django==1!1")); - assertEquals(new PyRequirement("pinax-utils", "1.0b1.dev3"), PyRequirement.fromLine("pinax-utils==1.0b1.dev3")); - assertEquals(new PyRequirement("Flask-Celery-py3", "0.1.*"), PyRequirement.fromLine("Flask-Celery-py3==0.1.*")); - assertEquals(new PyRequirement("no_limit_nester", "1.0+local.version.10"), - PyRequirement.fromLine("no_limit_nester==1.0+local.version.10")); + 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 @@ -2116,7 +2105,7 @@ public class PyRequirementTest extends PyTestCase { // PY-20223 public void testRequirementVersionWithBigInteger() { - assertEquals(new PyRequirement("pkg-name", "3.4.201607251407"), PyRequirement.fromLine("pkg-name==3.4.201607251407")); + assertEquals(pyRequirement("pkg-name", EQ, "3.4.201607251407"), fromLine("pkg-name==3.4.201607251407")); } // PY-11835 @@ -2124,55 +2113,44 @@ public class PyRequirementTest extends PyTestCase { final String name = "django_compressor"; final String version = "dev"; final String line = name + "==" + version; - final List versionSpecs = Collections.singletonList(new PyRequirementVersionSpec(version)); + final List versionSpecs = singletonList(pyRequirementVersionSpec(STR_EQ, version)); - assertEquals(new PyRequirement(name, versionSpecs, Collections.singletonList(line)), PyRequirement.fromLine(line)); + assertEquals(new PyRequirementImpl(name, versionSpecs, singletonList(line), ""), fromLine(line)); } // https://www.python.org/dev/peps/pep-0440/#version-specifiers public void testRequirementRelation() { - doRequirementRelationTest(PyRequirementRelation.LT, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(PyRequirementRelation.LTE, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(PyRequirementRelation.NE, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(PyRequirementRelation.EQ, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(PyRequirementRelation.GT, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(PyRequirementRelation.GTE, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(PyRequirementRelation.COMPATIBLE, PyRequirementVersion.release("1.*")); - doRequirementRelationTest(PyRequirementRelation.STR_EQ, PyRequirementVersion.release("version")); + doRequirementRelationTest(LT, release("1.4")); + doRequirementRelationTest(LTE, release("1.4")); + doRequirementRelationTest(NE, release("1.4")); + doRequirementRelationTest(EQ, release("1.4")); + doRequirementRelationTest(GT, release("1.4")); + doRequirementRelationTest(GTE, release("1.4")); + doRequirementRelationTest(COMPATIBLE, release("1.*")); - doRequirementRelationTest(Arrays.asList(PyRequirementRelation.GTE, PyRequirementRelation.EQ), - Arrays.asList(PyRequirementVersion.release("2.8.1"), PyRequirementVersion.release("2.8.*"))); - doRequirementRelationTest(Arrays.asList(PyRequirementRelation.LT, PyRequirementRelation.GTE), - Arrays.asList(PyRequirementVersion.release("1.4"), PyRequirementVersion.release("1.3.1"))); + assertEquals(pyRequirement("name", STR_EQ, "version"), fromLine("name===version")); - doRequirementRelationTest(Arrays.asList(PyRequirementRelation.LT, - PyRequirementRelation.GT, - PyRequirementRelation.NE, - PyRequirementRelation.LT, - PyRequirementRelation.EQ), - Arrays.asList(PyRequirementVersion.release("1.6"), - PyRequirementVersion.release("1.9"), - PyRequirementVersion.release("1.9.6"), - new PyRequirementVersion(null, "2.0", "a0", null, null, null), - new PyRequirementVersion(null, "2.4", "rc1", null, null, null))); + doRequirementRelationTest(Arrays.asList(GTE, EQ), Arrays.asList(release("2.8.1"), release("2.8.*"))); + doRequirementRelationTest(Arrays.asList(LT, GTE), Arrays.asList(release("1.4"), release("1.3.1"))); + + doRequirementRelationTest(Arrays.asList(LT, GT, NE, LT, EQ), + Arrays.asList(release("1.6"), + release("1.9"), + release("1.9.6"), + new PyPackageVersion(null, "2.0", "a0", null, null, null), + new PyPackageVersion(null, "2.4", "rc1", null, null, null))); // PY-14583 - doRequirementRelationTest(Arrays.asList(PyRequirementRelation.GTE, - PyRequirementRelation.LTE, - PyRequirementRelation.GTE, - PyRequirementRelation.LTE), - Arrays.asList(PyRequirementVersion.release("0.8.4"), - PyRequirementVersion.release("0.8.99"), - PyRequirementVersion.release("0.9.7"), - PyRequirementVersion.release("0.9.99"))); + doRequirementRelationTest(Arrays.asList(GTE, LTE, GTE, LTE), + Arrays.asList(release("0.8.4"), release("0.8.99"), release("0.9.7"), release("0.9.99"))); } // https://www.python.org/dev/peps/pep-0508/#extras // PY-15674 public void testRequirementExtras() { final String name = "MyProject1"; - final List relations = Collections.emptyList(); - final List versions = Collections.emptyList(); + final List relations = emptyList(); + final List versions = emptyList(); doRequirementRelationTest(name, "[PDF]", relations, versions); doRequirementRelationTest(name, " [extra1, extra2]", relations, versions); @@ -2191,49 +2169,42 @@ public class PyRequirementTest extends PyTestCase { final String extras3 = " [security,tests]"; final String name3 = "requests"; - doRequirementRelationTest(name1, extras1, PyRequirementRelation.LT, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(name2, extras2, PyRequirementRelation.LTE, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(name3, extras3, PyRequirementRelation.NE, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(name1, extras1, PyRequirementRelation.EQ, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(name2, extras2, PyRequirementRelation.GT, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(name3, extras3, PyRequirementRelation.GTE, PyRequirementVersion.release("1.4")); - doRequirementRelationTest(name1, extras1, PyRequirementRelation.COMPATIBLE, PyRequirementVersion.release("1.*")); - doRequirementRelationTest(name2, extras2, PyRequirementRelation.STR_EQ, PyRequirementVersion.release("version")); + doRequirementRelationTest(name1, extras1, LT, release("1.4")); + doRequirementRelationTest(name2, extras2, LTE, release("1.4")); + doRequirementRelationTest(name3, extras3, NE, release("1.4")); + doRequirementRelationTest(name1, extras1, EQ, release("1.4")); + doRequirementRelationTest(name2, extras2, GT, release("1.4")); + doRequirementRelationTest(name3, extras3, GTE, release("1.4")); + doRequirementRelationTest(name1, extras1, COMPATIBLE, release("1.*")); - doRequirementRelationTest(name3, - extras3, - Arrays.asList(PyRequirementRelation.GTE, PyRequirementRelation.EQ), - Arrays.asList(PyRequirementVersion.release("2.8.1"), PyRequirementVersion.release("2.8.*"))); + final String line1 = name2 + extras2 + STR_EQ.getPresentableText() + "version"; + assertEquals( + new PyRequirementImpl( + name2, + singletonList(pyRequirementVersionSpec(STR_EQ, "version")), + singletonList(line1), + StringUtil.trimLeading(extras2) + ), + fromLine(line1) + ); - doRequirementRelationTest(name1, - extras1, - Arrays.asList(PyRequirementRelation.LT, PyRequirementRelation.GTE), - Arrays.asList(PyRequirementVersion.release("1.4"), PyRequirementVersion.release("1.3.1"))); + doRequirementRelationTest(name3, extras3, Arrays.asList(GTE, EQ), Arrays.asList(release("2.8.1"), release("2.8.*"))); + doRequirementRelationTest(name1, extras1, Arrays.asList(LT, GTE), Arrays.asList(release("1.4"), release("1.3.1"))); doRequirementRelationTest(name2, extras2, - Arrays.asList(PyRequirementRelation.LT, - PyRequirementRelation.GT, - PyRequirementRelation.NE, - PyRequirementRelation.LT, - PyRequirementRelation.EQ), - Arrays.asList(PyRequirementVersion.release("1.6"), - PyRequirementVersion.release("1.9"), - PyRequirementVersion.release("1.9.6"), - new PyRequirementVersion(null, "2.0", "a0", null, null, null), - new PyRequirementVersion(null, "2.4", "rc1", null, null, null))); + Arrays.asList(LT, GT, NE, LT, EQ), + Arrays.asList(release("1.6"), + release("1.9"), + release("1.9.6"), + new PyPackageVersion(null, "2.0", "a0", null, null, null), + new PyPackageVersion(null, "2.4", "rc1", null, null, null))); // PY-14583 doRequirementRelationTest(name3, extras3, - Arrays.asList(PyRequirementRelation.GTE, - PyRequirementRelation.LTE, - PyRequirementRelation.GTE, - PyRequirementRelation.LTE), - Arrays.asList(PyRequirementVersion.release("0.8.4"), - PyRequirementVersion.release("0.8.99"), - PyRequirementVersion.release("0.9.7"), - PyRequirementVersion.release("0.9.99"))); + Arrays.asList(GTE, LTE, GTE, LTE), + Arrays.asList(release("0.8.4"), release("0.8.99"), release("0.9.7"), release("0.9.99"))); } // https://pip.pypa.io/en/stable/reference/pip_install/#per-requirement-overrides @@ -2242,8 +2213,7 @@ public class PyRequirementTest extends PyTestCase { final String version = "1.2"; final String linePrefix = name + " >= " + version; - final List versionSpecs = - Collections.singletonList(new PyRequirementVersionSpec(PyRequirementRelation.GTE, PyRequirementVersion.release(version))); + final List versionSpecs = singletonList(pyRequirementVersionSpec(GTE, release(version))); final List installOptions1 = Arrays.asList(linePrefix, "--global-option", "--no-user-cfg", @@ -2253,11 +2223,11 @@ public class PyRequirementTest extends PyTestCase { "--global-option=\"--no-user-cfg\" " + "--install-option=\"--prefix='/usr/local'\" " + "--install-option=\"--no-compile\""; - assertEquals(new PyRequirement(name, versionSpecs, installOptions1), PyRequirement.fromLine(line1)); + assertEquals(new PyRequirementImpl(name, versionSpecs, installOptions1, ""), fromLine(line1)); final List installOptions2 = Arrays.asList(linePrefix, "--install-option", "--install-scripts=/usr/local/bin"); final String line2 = linePrefix + " --install-option=\"--install-scripts=/usr/local/bin\""; - assertEquals(new PyRequirement(name, versionSpecs, installOptions2), PyRequirement.fromLine(line2)); + assertEquals(new PyRequirementImpl(name, versionSpecs, installOptions2, ""), fromLine(line2)); } public void testMultilineRequirement() { @@ -2265,8 +2235,7 @@ public class PyRequirementTest extends PyTestCase { final String version = "1.2"; final String textPrefix = name + " >= " + version; - final List versionSpecs = - Collections.singletonList(new PyRequirementVersionSpec(PyRequirementRelation.GTE, PyRequirementVersion.release(version))); + final List versionSpecs = singletonList(pyRequirementVersionSpec(GTE, release(version))); final String text = textPrefix + " " + "--global-option=\"--no-user-cfg\" \\\n" + @@ -2278,133 +2247,133 @@ public class PyRequirementTest extends PyTestCase { "--install-option", "--prefix='/usr/local'", "--install-option", "--no-compile"); - assertEquals(Collections.singletonList(new PyRequirement(name, versionSpecs, installOptions)), PyRequirement.fromText(text)); + assertEquals(singletonList(new PyRequirementImpl(name, versionSpecs, installOptions, "")), PyRequirementParser.fromText(text)); } // PY-6355 public void testTrailingZeroesInVersion() { - final PyRequirement req = fix(PyRequirement.fromLine("foo==0.8.0")); - final PyPackage pkg = new PyPackage("foo", "0.8", null, Collections.emptyList()); + final PyRequirement req = fromLine("foo==0.8.0"); + final PyPackage pkg = new PyPackage("foo", "0.8", null, emptyList()); assertNotNull(req); - assertEquals(pkg, req.match(Collections.singletonList(pkg))); + assertEquals(pkg, req.match(singletonList(pkg))); } // PY-6438 public void testUnderscoreMatchesDash() { - final PyRequirement req = fix(PyRequirement.fromLine("pyramid_zcml")); - final PyPackage pkg = new PyPackage("pyramid-zcml", "0.1", null, Collections.emptyList()); + final PyRequirement req = fromLine("pyramid_zcml"); + final PyPackage pkg = new PyPackage("pyramid-zcml", "0.1", null, emptyList()); assertNotNull(req); - assertEquals(pkg, req.match(Collections.singletonList(pkg))); + assertEquals(pkg, req.match(singletonList(pkg))); } // PY-20242 public void testVersionInterpretedAsString() { - final PyRequirement req = fix(PyRequirement.fromLine("foo===version")); - final PyPackage pkg = new PyPackage("foo", "version", null, Collections.emptyList()); + final PyRequirement req = fromLine("foo===version"); + final PyPackage pkg = new PyPackage("foo", "version", null, emptyList()); assertNotNull(req); - assertEquals(pkg, req.match(Collections.singletonList(pkg))); + assertEquals(pkg, req.match(singletonList(pkg))); } // PY-20880 public void testMatchingLocalVersions() { - final PyPackage firstPackageWithLocalVersion = new PyPackage("foo", "1.0+foo0100", null, Collections.emptyList()); - final PyPackage secondPackageWithLocalVersion = new PyPackage("foo", "1.0+foo0101", null, Collections.emptyList()); + final PyPackage firstPackageWithLocalVersion = new PyPackage("foo", "1.0+foo0100", null, emptyList()); + final PyPackage secondPackageWithLocalVersion = new PyPackage("foo", "1.0+foo0101", null, emptyList()); - final PyRequirement requirement = fix(PyRequirement.fromLine("foo==1.0")); - assertEquals(firstPackageWithLocalVersion, requirement.match(Collections.singletonList(firstPackageWithLocalVersion))); - assertEquals(secondPackageWithLocalVersion, requirement.match(Collections.singletonList(secondPackageWithLocalVersion))); + final PyRequirement requirement = fromLine("foo==1.0"); + assertEquals(firstPackageWithLocalVersion, requirement.match(singletonList(firstPackageWithLocalVersion))); + assertEquals(secondPackageWithLocalVersion, requirement.match(singletonList(secondPackageWithLocalVersion))); - final PyRequirement requirementWithLocalVersion = fix(PyRequirement.fromLine("foo==1.0+foo0100")); - assertEquals(firstPackageWithLocalVersion, requirementWithLocalVersion.match(Collections.singletonList(firstPackageWithLocalVersion))); - assertNull(requirementWithLocalVersion.match(Collections.singletonList(secondPackageWithLocalVersion))); + final PyRequirement requirementWithLocalVersion = fromLine("foo==1.0+foo0100"); + assertEquals(firstPackageWithLocalVersion, requirementWithLocalVersion.match(singletonList(firstPackageWithLocalVersion))); + assertNull(requirementWithLocalVersion.match(singletonList(secondPackageWithLocalVersion))); } // https://www.python.org/dev/peps/pep-0440/#version-matching // PY-22275 public void testMatchingStar() { - final PyRequirement requirement = fix(PyRequirement.fromLine("foo==1.1.*")); - final PyPackage release = new PyPackage("foo", "1.1.2", null, Collections.emptyList()); - final PyPackage pre = new PyPackage("foo", "1.1.2a1", null, Collections.emptyList()); - final PyPackage post = new PyPackage("foo", "1.1.2.post1", null, Collections.emptyList()); - final PyPackage dev = new PyPackage("foo", "1.1.2.dev1", null, Collections.emptyList()); - final PyPackage localVersion = new PyPackage("foo", "1.1.2+local.version", null, Collections.emptyList()); + final PyRequirement requirement = fromLine("foo==1.1.*"); + final PyPackage release = new PyPackage("foo", "1.1.2", null, emptyList()); + final PyPackage pre = new PyPackage("foo", "1.1.2a1", null, emptyList()); + final PyPackage post = new PyPackage("foo", "1.1.2.post1", null, emptyList()); + final PyPackage dev = new PyPackage("foo", "1.1.2.dev1", null, emptyList()); + final PyPackage localVersion = new PyPackage("foo", "1.1.2+local.version", null, emptyList()); - assertEquals(release, requirement.match(Collections.singletonList(release))); - assertEquals(pre, requirement.match(Collections.singletonList(pre))); - assertEquals(post, requirement.match(Collections.singletonList(post))); - assertEquals(dev, requirement.match(Collections.singletonList(dev))); - assertEquals(localVersion, requirement.match(Collections.singletonList(localVersion))); + assertEquals(release, requirement.match(singletonList(release))); + assertEquals(pre, requirement.match(singletonList(pre))); + assertEquals(post, requirement.match(singletonList(post))); + assertEquals(dev, requirement.match(singletonList(dev))); + assertEquals(localVersion, requirement.match(singletonList(localVersion))); - final PyRequirement negativeRequirement = fix(PyRequirement.fromLine("foo!=1.1.*")); - final PyPackage negativeRelease = new PyPackage("foo", "1.2.2", null, Collections.emptyList()); - final PyPackage negativePre = new PyPackage("foo", "1.2.2a1", null, Collections.emptyList()); - final PyPackage negativePost = new PyPackage("foo", "1.2.2.post1", null, Collections.emptyList()); - final PyPackage negativeDev = new PyPackage("foo", "1.2.2.dev1", null, Collections.emptyList()); - final PyPackage negativeLocalVersion = new PyPackage("foo", "1.2.2+local.version", null, Collections.emptyList()); + final PyRequirement negativeRequirement = fromLine("foo!=1.1.*"); + final PyPackage negativeRelease = new PyPackage("foo", "1.2.2", null, emptyList()); + final PyPackage negativePre = new PyPackage("foo", "1.2.2a1", null, emptyList()); + final PyPackage negativePost = new PyPackage("foo", "1.2.2.post1", null, emptyList()); + final PyPackage negativeDev = new PyPackage("foo", "1.2.2.dev1", null, emptyList()); + final PyPackage negativeLocalVersion = new PyPackage("foo", "1.2.2+local.version", null, emptyList()); assertNull(negativeRequirement.match(Arrays.asList(release, pre, post, dev, localVersion))); - assertEquals(negativeRelease, negativeRequirement.match(Collections.singletonList(negativeRelease))); - assertEquals(negativePre, negativeRequirement.match(Collections.singletonList(negativePre))); - assertEquals(negativePost, negativeRequirement.match(Collections.singletonList(negativePost))); - assertEquals(negativeDev, negativeRequirement.match(Collections.singletonList(negativeDev))); - assertEquals(negativeLocalVersion, negativeRequirement.match(Collections.singletonList(negativeLocalVersion))); + assertEquals(negativeRelease, negativeRequirement.match(singletonList(negativeRelease))); + assertEquals(negativePre, negativeRequirement.match(singletonList(negativePre))); + assertEquals(negativePost, negativeRequirement.match(singletonList(negativePost))); + assertEquals(negativeDev, negativeRequirement.match(singletonList(negativeDev))); + assertEquals(negativeLocalVersion, negativeRequirement.match(singletonList(negativeLocalVersion))); } // https://www.python.org/dev/peps/pep-0440/#compatible-release // PY-20522 public void testMatchingCompatible() { - final PyRequirement requirement = fix(PyRequirement.fromLine("foo~=2.2")); - final PyPackage release = new PyPackage("foo", "2.3", null, Collections.emptyList()); - final PyPackage pre = new PyPackage("foo", "2.3a1", null, Collections.emptyList()); - final PyPackage post = new PyPackage("foo", "2.3.post1", null, Collections.emptyList()); - final PyPackage dev = new PyPackage("foo", "2.3.dev1", null, Collections.emptyList()); - final PyPackage localVersion = new PyPackage("foo", "2.3+local.version", null, Collections.emptyList()); + final PyRequirement requirement = fromLine("foo~=2.2"); + final PyPackage release = new PyPackage("foo", "2.3", null, emptyList()); + final PyPackage pre = new PyPackage("foo", "2.3a1", null, emptyList()); + final PyPackage post = new PyPackage("foo", "2.3.post1", null, emptyList()); + final PyPackage dev = new PyPackage("foo", "2.3.dev1", null, emptyList()); + final PyPackage localVersion = new PyPackage("foo", "2.3+local.version", null, emptyList()); - assertEquals(release, requirement.match(Collections.singletonList(release))); - assertEquals(pre, requirement.match(Collections.singletonList(pre))); - assertEquals(post, requirement.match(Collections.singletonList(post))); - assertEquals(dev, requirement.match(Collections.singletonList(dev))); - assertEquals(localVersion, requirement.match(Collections.singletonList(localVersion))); + assertEquals(release, requirement.match(singletonList(release))); + assertEquals(pre, requirement.match(singletonList(pre))); + assertEquals(post, requirement.match(singletonList(post))); + assertEquals(dev, requirement.match(singletonList(dev))); + assertEquals(localVersion, requirement.match(singletonList(localVersion))); - final PyRequirement moreModernRequirement = fix(PyRequirement.fromLine("foo~=2.4")); + final PyRequirement moreModernRequirement = fromLine("foo~=2.4"); assertNull(moreModernRequirement.match(Arrays.asList(release, pre, post, dev, localVersion))); } // https://www.python.org/dev/peps/pep-0440/#compatible-release // PY-20522 public void testMatchingCompatibleWithTrailingZero() { - final PyRequirement requirement = fix(PyRequirement.fromLine("foo~=2.20.0")); - final PyPackage release = new PyPackage("foo", "2.20.3", null, Collections.emptyList()); - final PyPackage pre = new PyPackage("foo", "2.20.3a1", null, Collections.emptyList()); - final PyPackage post = new PyPackage("foo", "2.20.3.post1", null, Collections.emptyList()); - final PyPackage dev = new PyPackage("foo", "2.20.3.dev1", null, Collections.emptyList()); - final PyPackage localVersion = new PyPackage("foo", "2.20.3+local.version", null, Collections.emptyList()); + final PyRequirement requirement = fromLine("foo~=2.20.0"); + final PyPackage release = new PyPackage("foo", "2.20.3", null, emptyList()); + final PyPackage pre = new PyPackage("foo", "2.20.3a1", null, emptyList()); + final PyPackage post = new PyPackage("foo", "2.20.3.post1", null, emptyList()); + final PyPackage dev = new PyPackage("foo", "2.20.3.dev1", null, emptyList()); + final PyPackage localVersion = new PyPackage("foo", "2.20.3+local.version", null, emptyList()); - assertEquals(release, requirement.match(Collections.singletonList(release))); - assertEquals(pre, requirement.match(Collections.singletonList(pre))); - assertEquals(post, requirement.match(Collections.singletonList(post))); - assertEquals(dev, requirement.match(Collections.singletonList(dev))); - assertEquals(localVersion, requirement.match(Collections.singletonList(localVersion))); + assertEquals(release, requirement.match(singletonList(release))); + assertEquals(pre, requirement.match(singletonList(pre))); + assertEquals(post, requirement.match(singletonList(post))); + assertEquals(dev, requirement.match(singletonList(dev))); + assertEquals(localVersion, requirement.match(singletonList(localVersion))); - final PyRequirement moreModernRequirement = fix(PyRequirement.fromLine("foo~=2.21.0")); + final PyRequirement moreModernRequirement = fromLine("foo~=2.21.0"); assertNull(moreModernRequirement.match(Arrays.asList(release, pre, post, dev, localVersion))); } // PY-27076 public void testMatchingAsteriskAndCompatibleWithTwoTrailingZeros() { - final PyRequirement requirement1 = fix(PyRequirement.fromLine("social-auth-app-django==2.0.*")); - final PyRequirement requirement2 = fix(PyRequirement.fromLine("social-auth-app-django~=2.0.0")); + final PyRequirement requirement1 = fromLine("social-auth-app-django==2.0.*"); + final PyRequirement requirement2 = fromLine("social-auth-app-django~=2.0.0"); - final PyPackage pkg = new PyPackage("social-auth-app-django", "2.0.0", null, Collections.emptyList()); + final PyPackage pkg = new PyPackage("social-auth-app-django", "2.0.0", null, emptyList()); - assertEquals(pkg, requirement1.match(Collections.singletonList(pkg))); - assertEquals(pkg, requirement2.match(Collections.singletonList(pkg))); + assertEquals(pkg, requirement1.match(singletonList(pkg))); + assertEquals(pkg, requirement2.match(singletonList(pkg))); } // OPTIONS public void testOptions() { assertEmpty( - PyRequirement.fromText( + PyRequirementParser.fromText( "-i URL\n" + "--index-url URL\n" + "--extra-index-url URL\n" + @@ -2425,13 +2394,13 @@ public class PyRequirementTest extends PyTestCase { final VirtualFile requirementsFile = getVirtualFileByName(getTestDataPath() + "/requirement/recursive/requirements.txt"); assertNotNull(requirementsFile); - assertEquals(Arrays.asList(new PyRequirement("bitly_api"), new PyRequirement("numpy"), new PyRequirement("SomeProject")), - PyRequirement.fromFile(requirementsFile)); + assertEquals(Arrays.asList(pyRequirement("bitly_api"), pyRequirement("numpy"), pyRequirement("SomeProject")), + PyRequirementParser.fromFile(requirementsFile)); } // COMMENTS public void testComment() { - assertNull(PyRequirement.fromLine("# comment")); + assertNull(fromLine("# comment")); } public void testCommentAtTheEnd() { @@ -2463,29 +2432,28 @@ public class PyRequirementTest extends PyTestCase { doCommentAtTheEndTest(name + " # comment"); doCommentAtTheEndTest(name, version, name + "==" + version + " # comment"); - assertEquals(new PyRequirement(name, Collections.emptyList(), Collections.singletonList(name + "[PDF]"), "[PDF]"), - PyRequirement.fromLine(name + "[PDF] # comment")); + assertEquals(new PyRequirementImpl(name, emptyList(), singletonList(name + "[PDF]"), "[PDF]"), + fromLine(name + "[PDF] # comment")); - final PyRequirement requirement = new PyRequirement(name, Collections.emptyList(), Arrays.asList(name, "--install-option", "option")); + final PyRequirement requirement = new PyRequirementImpl(name, emptyList(), Arrays.asList(name, "--install-option", "option"), ""); - assertEquals(requirement, PyRequirement.fromLine(name + " --install-option=\"option\" # comment")); - assertEquals(Collections.singletonList(requirement), - PyRequirement.fromText(name + " \\\n--install-option=\"option\" # comment")); + assertEquals(requirement, fromLine(name + " --install-option=\"option\" # comment")); + assertEquals(singletonList(requirement), PyRequirementParser.fromText(name + " \\\n--install-option=\"option\" # comment")); } // ENV MARKERS // TODO: https://www.python.org/dev/peps/pep-0426/#environment-markers, https://www.python.org/dev/peps/pep-0508/#environment-markers private static void doTest(@NotNull String line) { - assertEquals(new PyRequirement("MyProject1", Collections.emptyList(), Arrays.asList(line.split("\\s+"))), PyRequirement.fromLine(line)); + assertEquals(new PyRequirementImpl("MyProject1", emptyList(), Arrays.asList(line.split("\\s+")), ""), fromLine(line)); } private static void doTest(@NotNull String name, @NotNull String version, @NotNull String line) { - assertEquals(new PyRequirement(name, version, Collections.singletonList(line)), PyRequirement.fromLine(line)); + final PyRequirementVersionSpec versionSpec = pyRequirementVersionSpec(EQ, version); + assertEquals(new PyRequirementImpl(name, singletonList(versionSpec), singletonList(line), ""), fromLine(line)); } - private static void doRequirementVersionNormalizationTest(@NotNull String expectedVersion, - @NotNull String actualVersion) { + private static void doRequirementVersionNormalizationTest(@NotNull String expectedVersion, @NotNull String actualVersion) { final String name = "name"; doTest(name, expectedVersion, name + "==" + actualVersion); } @@ -2498,26 +2466,25 @@ public class PyRequirementTest extends PyTestCase { doTest(name, version, line.substring(0, line.lastIndexOf('#') - 1)); } - private static void doRequirementRelationTest(@NotNull PyRequirementRelation relation, @NotNull PyRequirementVersion version) { - doRequirementRelationTest("Django", null, Collections.singletonList(relation), Collections.singletonList(version)); + private static void doRequirementRelationTest(@NotNull PyRequirementRelation relation, @NotNull PyPackageVersion version) { + doRequirementRelationTest("Django", null, singletonList(relation), singletonList(version)); } - private static void doRequirementRelationTest(@NotNull List relations, - @NotNull List versions) { + private static void doRequirementRelationTest(@NotNull List relations, @NotNull List versions) { doRequirementRelationTest("Django", null, relations, versions); } private static void doRequirementRelationTest(@NotNull String name, @Nullable String extras, @NotNull PyRequirementRelation relation, - @NotNull PyRequirementVersion version) { - doRequirementRelationTest(name, extras, Collections.singletonList(relation), Collections.singletonList(version)); + @NotNull PyPackageVersion version) { + doRequirementRelationTest(name, extras, singletonList(relation), singletonList(version)); } private static void doRequirementRelationTest(@NotNull String name, @Nullable String extras, @NotNull List relations, - @NotNull List versions) { + @NotNull List versions) { assertEquals(versions.size(), relations.size()); final StringBuilder sb = new StringBuilder(name); @@ -2525,23 +2492,28 @@ public class PyRequirementTest extends PyTestCase { if (extras != null) sb.append(extras); - for (Pair pair : ContainerUtil.zip(relations, versions)) { + for (Pair pair : ContainerUtil.zip(relations, versions)) { final PyRequirementRelation relation = pair.getFirst(); - final PyRequirementVersion version = pair.getSecond(); + final PyPackageVersion version = pair.getSecond(); - expectedVersionSpecs.add(new PyRequirementVersionSpec(relation, version)); + expectedVersionSpecs.add(pyRequirementVersionSpec(relation, version)); } - sb.append(StringUtil.join(expectedVersionSpecs, ",")); + sb.append(StringUtil.join(expectedVersionSpecs, PyRequirementVersionSpec::getPresentableText, ",")); final String options = sb.toString(); if (extras == null) { - assertEquals(new PyRequirement(name, expectedVersionSpecs, Collections.singletonList(options)), PyRequirement.fromLine(options)); + assertEquals(new PyRequirementImpl(name, expectedVersionSpecs, singletonList(options), ""), fromLine(options)); } else { - assertEquals(new PyRequirement(name, expectedVersionSpecs, Collections.singletonList(options), StringUtil.trimLeading(extras)), - PyRequirement.fromLine(options)); + assertEquals(new PyRequirementImpl(name, expectedVersionSpecs, singletonList(options), StringUtil.trimLeading(extras)), + fromLine(options)); } } + + @NotNull + private static PyPackageVersion release(@NotNull String version) { + return new PyPackageVersion(null, version, null, null, null, null); + } }