small fixes for python requirements generation (PY-18847)

GitOrigin-RevId: c538c855d1bf220981eb430371d55f7f499bf12b
This commit is contained in:
Aleksei Kniazev
2020-02-26 17:03:35 +00:00
committed by intellij-monorepo-bot
parent 3a6eab25cd
commit 7b8adcf536
7 changed files with 30 additions and 108 deletions
@@ -1496,6 +1496,7 @@ integrated.tools.configurable.docstrings=Docstrings
integrated.tools.configurable.restructuredtext=reStructuredText
integrated.tools.configurable.packaging=Packaging
integrated.tools.configurable.testing=Testing
integrated.tools.configurable.pipenv=Pipenv
command.line.parser.error.message=Space between argument is its value is unexpected
python.template.package.installation.validation.message="{0} will be installed on selected interpreter"
@@ -1521,9 +1522,9 @@ python.requirements.version.separator.compatible=Compatible version
python.requirements.remove.unused=Remove unused requirements
python.requirements.modify.base.files=Modify base files (defined with -r or --requirement)
python.requirements.keep.matching.specifier=Keep existing version specifier if it matches the current version
python.requirements.quickfix.family.name=Synchronize requirements with project imports
python.requirements.quickfix.family.name=Add imported packages to requirements...
python.requirements.analyzing.imports.title=Analyzing imports in project
python.requirements.requirements.file.dialog.header=Select requirements file
python.requirements.action.name=Sync Python requirements
python.requirements.error.ends.with.slash=Error parsing requirements: file ends with '/' symbol.
python.requirements.error.no.packages=Error retrieving the list of packages.
python.requirements.error.no.interpreter=Configured interpreter required to synchronize requirements.
@@ -4777,4 +4777,5 @@ torch torch-nightly
can python-can
github PyGithub
gitlab python-gitlab
tensorflow tensorflow-gpu
tensorflow tensorflow-gpu
aocd advent-of-code-data
@@ -124,6 +124,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
myRestPanel.setBorder(IdeBorderFactory.createTitledBorder(PyBundle.message("integrated.tools.configurable.restructuredtext")));
myPackagingPanel.setBorder(IdeBorderFactory.createTitledBorder(PyBundle.message("integrated.tools.configurable.packaging")));
myTestsPanel.setBorder(IdeBorderFactory.createTitledBorder(PyBundle.message("integrated.tools.configurable.testing")));
myPipEnvPanel.setBorder(IdeBorderFactory.createTitledBorder(PyBundle.message("integrated.tools.configurable.pipenv")));
}
@NotNull
@@ -40,7 +40,6 @@ import com.jetbrains.python.sdk.PySdkExtKt;
import com.jetbrains.python.sdk.PythonSdkUtil;
import com.jetbrains.python.sdk.pipenv.PipEnvInstallQuickFix;
import com.jetbrains.python.sdk.pipenv.PipenvKt;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -224,24 +223,14 @@ public class PyPackageRequirementsInspection extends PyInspection {
}
}
final List<LocalQuickFix> quickFixes = new ArrayList<>();
StreamEx
.of(packageName)
.append(possiblePyPIPackageNames)
.filter(PyPIPackageUtil.INSTANCE::isInPyPI)
.map(name -> new AddToRequirementsFix(packageManager, module, name, LanguageLevel.forElement(importedExpression)))
.forEach(quickFixes::add);
quickFixes.add(new PyGenerateRequirementsFileQuickFix(module));
quickFixes.add(new IgnoreRequirementFix(Collections.singleton(packageName)));
final LocalQuickFix[] fixes = { new PyGenerateRequirementsFileQuickFix(module),
new IgnoreRequirementFix(Collections.singleton(packageName))};
registerProblem(packageReferenceExpression,
String.format("Package containing module '%s' is not listed in project requirements", packageName),
ProblemHighlightType.WEAK_WARNING,
null,
quickFixes.toArray(LocalQuickFix.EMPTY_ARRAY));
fixes);
}
}
}
@@ -664,77 +653,4 @@ public class PyPackageRequirementsInspection extends PyInspection {
}
}
}
private static class AddToRequirementsFix implements LocalQuickFix {
@NotNull
private final PyPackageManager myPackageManager;
@NotNull
private final Module myModule;
@NotNull
private final String myPackageName;
@NotNull
private final LanguageLevel myLanguageLevel;
private AddToRequirementsFix(@NotNull PyPackageManager packageManager,
@NotNull Module module,
@NotNull String packageName,
@NotNull LanguageLevel languageLevel) {
myPackageManager = packageManager;
myModule = module;
myPackageName = packageName;
myLanguageLevel = languageLevel;
}
@Override
public boolean startInWriteAction() {
return false;
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getFamilyName() {
return PyBundle.message("INSP.package.requirements.add.requirement");
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getName() {
return String.format("Add requirement '%s' to %s", myPackageName, calculateTarget());
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final List<PyRequirement> requirements = myPackageManager.getRequirements(myModule);
if (requirements != null && ContainerUtil.exists(requirements, r -> r.getName().equals(myPackageName))) return;
CommandProcessor.getInstance().executeCommand(
project,
() -> ApplicationManager.getApplication().runWriteAction(
() -> PyPackageUtil.addRequirementToTxtOrSetupPy(myModule, myPackageName, myLanguageLevel)
),
getName(),
null
);
}
@NotNull
private String calculateTarget() {
final VirtualFile requirementsTxt = PyPackageUtil.findRequirementsTxt(myModule);
if (requirementsTxt != null) {
return requirementsTxt.getName();
}
else if (PyPackageUtil.findSetupCall(myModule) != null) {
return "setup.py";
}
else {
return "project requirements";
}
}
}
}
@@ -64,16 +64,12 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String,
}
// report those requirements that were not changed in base files
var inBaseFile = parsed.asSequence()
parsed.asSequence()
.filter { it.name.toLowerCase() in importedPackages }
.map { it to importedPackages[it.name.toLowerCase()] }
.map { it to importedPackages.remove(it.name.toLowerCase()) }
.filterNot { compatibleVersion(it.first, it.second!!.version, settings.specifyVersion) }
.forEach { unchangedInBaseFiles.add(it.first.name) }
if (settings.keepMatchingSpecifier) {
inBaseFile = inBaseFile.filterNot { compatibleVersion(it.first, it.second!!.version, settings.specifyVersion) }
}
inBaseFile.forEach { unchangedInBaseFiles.add(it.first.name) }
parsed.forEach { importedPackages.remove(it.name.toLowerCase()) }
outputLines.addAll(lines)
}
else if (parsed.isEmpty()) {
@@ -86,7 +82,12 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String,
// processing only requirements that are used in the project, discarding others
if (name in importedPackages) {
val pkg = importedPackages.remove(name)!!
if (settings.keepMatchingSpecifier && compatibleVersion(requirement, pkg.version, settings.specifyVersion)) {
if (requirement.isEditable || vcsPrefixes.any { line.startsWith(it) }) {
// keeping editable and vcs requirements
outputLines.addAll(lines)
}
else if (settings.keepMatchingSpecifier && compatibleVersion(requirement, pkg.version, settings.specifyVersion)) {
// existing version separators match the current package version, keeping them
outputLines.addAll(lines)
}
else {
@@ -124,7 +125,6 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String,
private fun convertToRequirementsEntry(requirement: PyRequirement, settings: PyPackageRequirementsSettings, version: String? = null): String {
val packageName = when {
requirement.isEditable -> "${requirement.installOptions[0]} ${requirement.installOptions[1]}${requirement.extras}"
settings.specifyVersion -> when {
version != null -> requirement.name + requirement.extras + settings.versionSpecifier.separator + version
else -> requirement.presentableText
@@ -132,11 +132,9 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String,
else -> requirement.name + requirement.extras
}
val optionsToDrop = if (requirement.isEditable) 2 else 1
if (requirement.installOptions.size == optionsToDrop) return packageName
if (requirement.installOptions.size == 1) return packageName
val offset = " ".repeat(packageName.length + 1)
val installOptions = requirement.installOptions.drop(optionsToDrop).joinToString(separator = "\\\n$offset")
val installOptions = requirement.installOptions.drop(1).joinToString(separator = "\\\n$offset")
return "$packageName $installOptions"
}
@@ -145,4 +143,8 @@ class PyRequirementsFileVisitor(private val importedPackages: MutableMap<String,
private fun isFileReference(line: String): Boolean = line.startsWith("-r ") || line.startsWith("--requirement ")
private fun isEditableSelf(line: String): Boolean = line.startsWith("--editable .") || line.startsWith("-e .")
companion object {
private val vcsPrefixes = listOf("git:", "git+", "svn+", "hg+", "bzr+")
}
}
@@ -54,7 +54,7 @@ fun syncWithImports(module: Module) {
if (matchResult == null) return
val psiManager = PsiManager.getInstance(module.project)
WriteCommandAction.runWriteCommandAction(module.project) {
WriteCommandAction.runWriteCommandAction(module.project, PyBundle.message("python.requirements.action.name"), null, {
if (requirementsFile == null) {
val path = Paths.get(settings.requirementsPath)
val location = when {
@@ -73,7 +73,7 @@ fun syncWithImports(module: Module) {
matchResult.baseFilesOutput.forEach { (file, content) ->
documentManager.getDocument(file)!!.setText(content.joinToString("\n"))
}
}
}, emptyArray())
psiManager.findFile(requirementsFile!!)?.navigate(true)
if (matchResult.unhandledLines.isNotEmpty()) {
val text = PyBundle.message("python.requirements.warning.unhandled.lines", matchResult.unhandledLines.joinToString(", "))
@@ -172,7 +172,7 @@ private fun showSpecifyRequirementsFileDialog(project: Project, settings: PyPack
{ settings.keepMatchingSpecifier = it })
}
}
val dialog = dialog(title = PyBundle.message("python.requirements.requirements.file.dialog.header"),
val dialog = dialog(title = PyBundle.message("python.requirements.action.name"),
panel = panel,
resizable = true,
project = project)
@@ -10,7 +10,7 @@ import org.easymock.EasyMock
class PyRequirementsGenerationTest : PyTestCase() {
var oldPackageManagers: PyPackageManagers? = null
private var oldPackageManagers: PyPackageManagers? = null
fun testNewFileGeneration() = doTest()
fun testNewFileWithoutVersion() = doTest(PyRequirementsVersionSpecifierType.NO_VERSION)
@@ -64,6 +64,7 @@ class PyRequirementsGenerationTest : PyTestCase() {
syncWithImports(myFixture.module)
myFixture.checkResultByFile("$testName/new_${settings.requirementsPath}")
}
assertProjectFilesNotParsed(myFixture.file)
}
finally {
settings.requirementsPath = oldRequirementsPath