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
This commit is contained in:
Semyon Proshev
2018-05-25 18:49:56 +03:00
parent cb547c0f7b
commit 2a94bba791
26 changed files with 1245 additions and 1426 deletions
@@ -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<IpnbRunConfiguration> implements DebugAwareConfiguration {
@NonNls private static final String ATTR_ADDITIONAL_OPTIONS = "additionalOptions";
@@ -95,9 +98,12 @@ public class IpnbRunConfiguration extends AbstractPythonRunConfiguration<IpnbRun
if (RemoteSdkCredentialsHolder.isRemoteSdk(sdk.getHomePath())) {
throw new RuntimeConfigurationError("Please select local python interpreter");
}
final List<PyPackage> packages = PyPackageManager.getInstance(sdk).getPackages();
final PyPackageManager packageManager = PyPackageManager.getInstance(sdk);
final List<PyPackage> 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<IpnbRun
if (version != null) {
final LanguageLevel level = LanguageLevel.fromPythonVersion(version);
if (level.isAtLeast(LanguageLevel.PYTHON33)) {
PyPackageManager.getInstance(sdk).install("jupyter");
packageManager.install("jupyter");
}
else {
PyPackageManager.getInstance(sdk).install(Lists.newArrayList(
PyRequirement.fromLine("ipython==5"), PyRequirement.fromLine("jupyter")),
Lists.newArrayList());
packageManager.install(
Arrays.asList(
pyRequirement("ipython", PyRequirementRelation.EQ, "5"),
pyRequirement("jupyter")
),
Collections.emptyList()
);
}
}
}
@@ -17,6 +17,7 @@ import com.intellij.execution.ExecutionException;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.messages.Topic;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -62,9 +63,39 @@ public abstract class PyPackageManager {
@Nullable
public abstract List<PyRequirement> getRequirements(@NotNull Module module);
/**
* @param line requirement description
* @return parsed requirement or null if given description could not be parsed.
* @see <a href="https://pip.pypa.io/en/stable/reference/pip_install/"><code>pip install</code> documentation</a>
* @see <a href="https://www.python.org/dev/peps/pep-0508/">PEP-508</a>
* @see <a href="https://www.python.org/dev/peps/pep-0440/">PEP-440</a>
*/
@Nullable
public abstract PyRequirement parseRequirement(@NotNull String line);
/**
* @param text requirements descriptions
* @return parsed requirements.
* <i>Note: the returned list does not contain null or repetitive values, descriptions that could not be parsed are skipped.</i>
* @see <a href="https://pip.pypa.io/en/stable/reference/pip_install/"><code>pip install</code> documentation</a>
* @see <a href="https://www.python.org/dev/peps/pep-0508/">PEP-508</a>
* @see <a href="https://www.python.org/dev/peps/pep-0440/">PEP-440</a>
*/
@NotNull
public abstract List<PyRequirement> parseRequirements(@NotNull String text);
/**
* @param file file containing requirements descriptions.
* Used as a foothold to resolve recursive requirements specified through <code>-r</code> or <code>--requirement</code> flags.
* @return parsed requirements.
* <i>Note: the returned list does not contain null or repetitive values, descriptions that could not be parsed are skipped.</i>
* @see <a href="https://pip.pypa.io/en/stable/reference/pip_install/"><code>pip install</code> documentation</a>
* @see <a href="https://www.python.org/dev/peps/pep-0508/">PEP-508</a>
* @see <a href="https://www.python.org/dev/peps/pep-0440/">PEP-440</a>
*/
@NotNull
public abstract List<PyRequirement> parseRequirements(@NotNull VirtualFile file);
@NotNull
public abstract Set<PyPackage> getDependents(@NotNull PyPackage pkg) throws ExecutionException;
@@ -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 <a href="https://pip.pypa.io/en/stable/reference/pip_install/"><code>pip install</code> documentation</a>
* @see <a href="https://www.python.org/dev/peps/pep-0508/">PEP-508</a>
* @see <a href="https://www.python.org/dev/peps/pep-0440/">PEP-440</a>
* @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<PyRequirementVersionSpec> myVersionSpecs;
@NotNull
private final List<String> myInstallOptions;
@NotNull
private final String myExtras;
List<PyRequirementVersionSpec> 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<String> 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<String> 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<PyRequirementVersionSpec> 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<PyRequirementVersionSpec> versionSpecs, @NotNull List<String> 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<PyRequirementVersionSpec> versionSpecs,
@NotNull List<String> 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 <code>pip install</code>.
* <i>
* Note:
* if list has more than one element it means that
* <code>--src</code>, <code>-e</code>, <code>--editable</code>, <code>--global-option</code> or <code>--install-option</code>
* options are used
* </i>.
*/
@NotNull
@Deprecated
public PyRequirement withVersionComparator(@NotNull Comparator<String> comparator) {
return new PyRequirement(myName,
ContainerUtil.map(myVersionSpecs, spec -> spec.withVersionComparator(comparator)), myInstallOptions, myExtras);
}
List<String> 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<PyRequirementVersionSpec> getVersionSpecs() {
return myVersionSpecs;
}
@NotNull
public List<String> 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<PyPackage> 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<? extends PyPackage> 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<PyRequirement> 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<PyRequirement> 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<PyRequirementVersionSpec> versionSpecs = parseVersionSpecs(matcher.group(REQUIREMENT_VERSIONS_SPECS_GROUP));
final List<String> 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<PyRequirement> fromText(@NotNull String text,
@Nullable VirtualFile containingFile,
@NotNull Set<VirtualFile> 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<String, String> parseNameAndVersionFromVcsOrArchive(@NotNull String name) {
boolean isName = true;
final List<String> nameParts = new ArrayList<>();
final List<String> 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<String, String> nameAndVersion,
@NotNull List<String> 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<PyRequirementVersionSpec> 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<String, String> nameAndVersion =
parseNameAndVersionFromVcsOrArchive(egg == null ? StringUtil.trimEnd(project, ".git") : egg);
return createVcsOrArchiveRequirement(nameAndVersion, calculateVcsInstallOptions(matcher), getVcsExtras(matcher));
}
@NotNull
private static List<PyRequirementVersionSpec> 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<String> calculateRequirementInstallOptions(@NotNull Matcher matcher) {
final List<String> 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<String> splitByLinesAndCollapse(@NotNull String text) {
final List<String> 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<PyRequirement> parseLine(@NotNull String line,
@Nullable VirtualFile containingFile,
@NotNull Set<VirtualFile> 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<String> nameParts) {
return normalizeName(StringUtil.join(nameParts, "-"));
}
@Nullable
private static String normalizeVcsOrArchiveVersionParts(@NotNull List<String> versionParts) {
return versionParts.isEmpty() ? null : normalizeVersion(StringUtil.join(versionParts, "-"));
}
@NotNull
private static List<String> calculateVcsInstallOptions(@NotNull Matcher matcher) {
final List<String> 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<PyRequirement> parseRecursiveLine(@NotNull String line,
@Nullable VirtualFile containingFile,
@NotNull Set<VirtualFile> 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, ",");
}
}
@@ -15,6 +15,11 @@ package com.jetbrains.python.packaging.requirement;
import org.jetbrains.annotations.NotNull;
/**
* @see <a href="https://www.python.org/dev/peps/pep-0440/#version-specifiers">PEP-440</a>
* @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 <a href="https://www.python.org/dev/peps/pep-0440/#version-specifiers">PEP-440</a>.
*/
@NotNull
@Override
public String toString() {
public String getPresentableText() {
return myValue;
}
}
@@ -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"
}
@@ -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 <a href="https://www.python.org/dev/peps/pep-0440/">PEP-440</a>
* @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<String> 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<String> 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<String> 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<String, String> publicAndLocalVersions = splitIntoPublicAndLocalVersions(myParsedVersion);
final Pair<String, String> 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<String, String> 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<String, String> 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();
}
}
@@ -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());
}
};
}
@@ -108,7 +108,7 @@ public class PyPackageRequirementsInspection extends PyInspection {
plural ? "are" : "is");
final Set<String> unsatisfiedNames = new HashSet<>();
for (PyRequirement req : unsatisfied) {
unsatisfiedNames.add(req.getFullName());
unsatisfiedNames.add(req.getName() + req.getExtras());
}
final List<LocalQuickFix> 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());
}
}
@@ -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<LocalQuickFix> actions, String packageName, Module module, Sdk sdk) {
final List<PyRequirement> requirements = Collections.singletonList(new PyRequirement(packageName));
final List<PyRequirement> requirements = Collections.singletonList(PyRequirementsKt.pyRequirement(packageName));
final String name = "Install package " + packageName;
actions.add(new PyPackageRequirementsInspection.PyInstallRequirementsFix(name, module, sdk, requirements));
}
@@ -61,7 +61,7 @@ public class PyCondaPackageManagerImpl extends PyPackageManagerImpl {
if (useConda) {
final ArrayList<String> 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<PyPackage> parseCondaToolOutput(@NotNull String s) throws ExecutionException {
private List<PyPackage> parseCondaToolOutput(@NotNull String s) throws ExecutionException {
final String[] lines = StringUtil.splitByLines(s);
final List<PyPackage> 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));
@@ -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<PyRequirement> parseRequirements(@NotNull String text) {
return PyPackageUtil.fix(PyRequirement.fromText(text));
return PyRequirementParser.fromText(text);
}
@NotNull
@Override
public List<PyRequirement> parseRequirements(@NotNull VirtualFile file) {
return PyRequirementParser.fromFile(file);
}
// public List<PyPackage> refreshAndGetPackagesIfNotInProgress(boolean alwaysRefresh) throws ExecutionException
@@ -547,7 +559,7 @@ public class PyPackageManagerImpl extends PyPackageManager {
}
@NotNull
private static List<PyPackage> parsePackagingToolOutput(@NotNull String s) throws ExecutionException {
private List<PyPackage> parsePackagingToolOutput(@NotNull String s) throws ExecutionException {
final String[] lines = StringUtil.splitByLines(s);
final List<PyPackage> 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));
@@ -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<PyRequirement> findSetupPyRequires(@NotNull Module module) {
final PyCallExpression setupCall = findSetupCall(module);
if (setupCall == null) return null;
if (setupCall == null) {
return null;
}
final List<PyRequirement> requirementsFromRequires = getSetupPyRequiresFromArguments(module, setupCall, SETUP_PY_REQUIRES_KWARGS_NAMES);
final List<PyRequirement> requirementsFromLinks = getSetupPyRequiresFromArguments(module, setupCall, DEPENDENCY_LINKS);
final List<PyRequirement> requirementsFromRequires = getSetupPyRequiresFromArguments(setupCall, SETUP_PY_REQUIRES_KWARGS_NAMES);
final List<PyRequirement> 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<String, List<PyRequirement>> 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<PyRequirement> getSetupPyRequiresFromArguments(@NotNull Module module,
@NotNull PyCallExpression setupCall,
private static List<PyRequirement> 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<PyRequirement> fix(@NotNull List<PyRequirement> requirements) {
return ContainerUtil.map(requirements, PyPackageUtil::fix);
}
@NotNull
private static List<PyRequirement> mergeSetupPyRequirements(@NotNull List<PyRequirement> requirementsFromRequires,
@NotNull List<PyRequirement> requirementsFromLinks) {
@@ -254,12 +226,12 @@ public class PyPackageUtil {
}
@Nullable
private static <T extends PyExpression> T resolveValue(@NotNull Module module, @Nullable PyExpression expression, @NotNull Class<T> cls) {
private static <T extends PyExpression> T resolveValue(@Nullable PyExpression expression, @NotNull Class<T> 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<PyRequirement> 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<PyRequirement> 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()) {
@@ -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" }
}
@@ -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<PyPackageVersion> {
/**
* Compares versions by normalizing them or using [com.intellij.webcore.packaging.PackageVersionComparator.VERSION_COMPARATOR] as a fallback
*/
@JvmStatic
val STR_COMPARATOR: Comparator<String> = 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<PyPackageVersion> {
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<PyPackageVersion> {
}
/**
* @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<PyPackageVersion> {
}
/**
* @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<PyPackageVersion> {
}
/**
* @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<PyPackageVersion> {
}
/**
* @see com.jetbrains.python.packaging.requirement.PyRequirementVersionNormalizer.normalizeLocal
* @see PyPackageVersionNormalizer.normalizeLocal
*/
private fun compareLocals(o1: PyPackageVersion, o2: PyPackageVersion) = (o1.local ?: "").compareTo(o2.local ?: "")
@@ -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;
* <a href="https://www.python.org/dev/peps/pep-0440/#normalization">https://www.python.org/dev/peps/pep-0440/#normalization</a>
* and
* <a href="https://www.python.org/dev/peps/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering">https://www.python.org/dev/peps/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering</a>.
*
* @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),
@@ -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<PyPackaging
.of(versions)
.findFirst(
version -> {
final PyRequirementVersion normalized = PyRequirementVersionNormalizer.normalize(version);
final PyPackageVersion normalized = PyPackageVersionNormalizer.normalize(version);
return normalized == null || normalized.getPre() == null && normalized.getDev() == null;
}
@@ -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<PyRequirementVersionSpec>,
private val installOptions: List<String>,
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>): PyPackage? {
val normalizedName = name.replace('_', '-')
return packages.firstOrNull { normalizedName.equals(it.name, true) && versionSpecs.all { spec -> spec.matches(it.version) } }
}
}
@@ -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 <a href="https://pip.pypa.io/en/stable/reference/pip_install/"><code>pip install</code> documentation</a>
* @see <a href="https://www.python.org/dev/peps/pep-0508/">PEP-508</a>
* @see <a href="https://www.python.org/dev/peps/pep-0440/">PEP-440</a>
* @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<PyRequirement> fromText(@NotNull String text) {
return fromText(text, null, new HashSet<>());
}
@NotNull
public static List<PyRequirement> 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<PyRequirementVersionSpec> versionSpecs = parseVersionSpecs(matcher.group(REQUIREMENT_VERSIONS_SPECS_GROUP));
final List<String> 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<PyRequirement> fromText(@NotNull String text,
@Nullable VirtualFile containingFile,
@NotNull Set<VirtualFile> 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<String, String> parseNameAndVersionFromVcsOrArchive(@NotNull String name) {
boolean isName = true;
final List<String> nameParts = new ArrayList<>();
final List<String> 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<String, String> nameAndVersion,
@NotNull List<String> 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<PyRequirementVersionSpec> 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<String, String> nameAndVersion =
parseNameAndVersionFromVcsOrArchive(egg == null ? StringUtil.trimEnd(project, ".git") : egg);
return createVcsOrArchiveRequirement(nameAndVersion, calculateVcsInstallOptions(matcher), getVcsExtras(matcher));
}
@NotNull
private static List<PyRequirementVersionSpec> 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<String> calculateRequirementInstallOptions(@NotNull Matcher matcher) {
final List<String> 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<String> splitByLinesAndCollapse(@NotNull String text) {
final List<String> 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<PyRequirement> parseLine(@NotNull String line,
@Nullable VirtualFile containingFile,
@NotNull Set<VirtualFile> 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<String> nameParts) {
return normalizeName(StringUtil.join(nameParts, "-"));
}
@Nullable
private static String normalizeVcsOrArchiveVersionParts(@NotNull List<String> versionParts) {
return versionParts.isEmpty() ? null : normalizeVersion(StringUtil.join(versionParts, "-"));
}
@NotNull
private static List<String> calculateVcsInstallOptions(@NotNull Matcher matcher) {
final List<String> 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<PyRequirement> parseRecursiveLine(@NotNull String line,
@Nullable VirtualFile containingFile,
@NotNull Set<VirtualFile> 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);
}
}
@@ -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<String, String> {
return version.copy(local = null).presentableText to StringUtil.notNullize(version.local)
}
private fun splitIntoPublicAndLocalVersions(version: String): Pair<String, String> {
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)
}
}
@@ -39,7 +39,7 @@ public class PyChooseRequirementsDialog extends DialogWrapper {
myRequirementsChooser = new ElementsChooser<PyRequirement>(true) {
@Override
public String getItemText(@NotNull PyRequirement requirement) {
return requirement.toString();
return requirement.getPresentableText();
}
};
myRequirementsChooser.setElements(requirements, true);
@@ -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
@@ -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<PyPackage> 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<PyPackage> 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<PyPackage> 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 <T> List<T> list(T... xs) {
return Arrays.asList(xs);
}
private abstract static class PyPackagingTestTask extends PyExecutionFixtureTestTask {
PyPackagingTestTask() {
super(null);
@@ -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;
}
@@ -55,7 +55,7 @@ public class PyPackageUtilTest extends PyTestCase {
// PY-18966
public void testSetupPyDependencyLinksReading() {
final List<PyRequirement> actual = PyPackageUtil.findSetupPyRequires(myFixture.getModule());
final List<PyRequirement> expected = PyRequirement.fromText(
final List<PyRequirement> 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<PyRequirement> actual, int fromIndex) {
final List<PyRequirement> expected = PyRequirement.fromText("Markdown\nNewDjango==1.3.1\nnumpy\nmynose");
final List<PyRequirement> 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<PyRequirement> actual = PyPackageUtil.findSetupPyRequires(module);
final List<PyRequirement> expected = PyRequirement.fromText("NewDjango==1.3.1\nMarkdown\nnumpy\nmynose");
final List<PyRequirement> expected = PyRequirementParser.fromText("NewDjango==1.3.1\nMarkdown\nnumpy\nmynose");
assertEquals(expected, actual);
}
@@ -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)
}
}
}
@@ -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<PyRequirementVersionSpec> versionSpecs = Collections.singletonList(new PyRequirementVersionSpec(version));
final List<PyRequirementVersionSpec> 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<PyRequirementRelation> relations = Collections.emptyList();
final List<PyRequirementVersion> versions = Collections.emptyList();
final List<PyRequirementRelation> relations = emptyList();
final List<PyPackageVersion> 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<PyRequirementVersionSpec> versionSpecs =
Collections.singletonList(new PyRequirementVersionSpec(PyRequirementRelation.GTE, PyRequirementVersion.release(version)));
final List<PyRequirementVersionSpec> versionSpecs = singletonList(pyRequirementVersionSpec(GTE, release(version)));
final List<String> 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<String> 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<PyRequirementVersionSpec> versionSpecs =
Collections.singletonList(new PyRequirementVersionSpec(PyRequirementRelation.GTE, PyRequirementVersion.release(version)));
final List<PyRequirementVersionSpec> 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<PyRequirementRelation> relations,
@NotNull List<PyRequirementVersion> versions) {
private static void doRequirementRelationTest(@NotNull List<PyRequirementRelation> relations, @NotNull List<PyPackageVersion> 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<PyRequirementRelation> relations,
@NotNull List<PyRequirementVersion> versions) {
@NotNull List<PyPackageVersion> 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<PyRequirementRelation, PyRequirementVersion> pair : ContainerUtil.zip(relations, versions)) {
for (Pair<PyRequirementRelation, PyPackageVersion> 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);
}
}