mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-88099 Normalize project names and improve new project wizard UX
- Add PyPackageName.normalizeProjectName() per PEP 503 name normalization spec - Normalize project names in uv, poetry, and hatch project generators - Fix hatch createNewProject to use NioFiles.copyRecursively instead of broken EEL move - Add background progress indicators for module structure creation and SDK setup - Refresh VFS with markDirtyAndRefresh after module structure creation - Add TraceContext to project generation and packaging tool window coroutines (cherry picked from commit b64dfb4ae541e4a220698d804aec3fe95103bdf2) IJ-MR-194426 GitOrigin-RevId: 0a1212d70d8136e1ba8821b736d09e00d78f310e
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1a6543038b
commit
67235b83d2
@@ -10,6 +10,22 @@ value class PyPackageName private constructor(val name: String) {
|
||||
fun from(name: String): PyPackageName =
|
||||
PyPackageName(normalizePackageName(name))
|
||||
|
||||
/**
|
||||
* Normalizes a project name according to
|
||||
* https://packaging.python.org/en/latest/specifications/name-normalization/#name-format
|
||||
*
|
||||
* Keeps only lowercase ASCII letters, digits, and hyphens.
|
||||
* Replaces everything else with a hyphen, collapses consecutive hyphens, and strips leading/trailing hyphens.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun normalizeProjectName(name: String): String {
|
||||
return name
|
||||
.lowercase()
|
||||
.replace(Regex("[^a-z0-9-]"), "-")
|
||||
.replace(Regex("-{2,}"), "-")
|
||||
.trim('-')
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun normalizePackageName(packageName: String): String {
|
||||
var name = packageName.trim()
|
||||
|
||||
@@ -1729,6 +1729,7 @@ read.only.python.sdk.system.wide.read.only.message=System Python packages are re
|
||||
trace.context.detecting.executable=Detecting {0} Executable
|
||||
trace.context.generating.git=Generating git
|
||||
trace.context.packaging.tool.window=Packaging Tool Window
|
||||
trace.context.packaging.tool.window.sdk.reload={0}: reload
|
||||
trace.context.packages.sdk.controller=Packages SDK Controller
|
||||
trace.context.add.local.python.sdk.dialog=Add Local Python SDK Dialog
|
||||
trace.context.add.remote.python.sdk.dialog=Add {0} Python SDK Dialog
|
||||
|
||||
@@ -40,8 +40,8 @@ class EnvironmentCreationHatchError(details: @NlsSafe String) : HatchError(
|
||||
PyHatchBundle.message("python.hatch.error.environment.creation", details)
|
||||
)
|
||||
|
||||
class FileSystemOperationHatchError(eelFsError: EelFsError) : HatchError(
|
||||
PyHatchBundle.message("python.hatch.error.filesystem.operation", eelFsError)
|
||||
class FileSystemOperationHatchError(details: @NlsSafe Any) : HatchError(
|
||||
PyHatchBundle.message("python.hatch.error.filesystem.operation", details)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.intellij.python.hatch.service
|
||||
|
||||
import com.intellij.openapi.util.io.NioFiles
|
||||
import com.intellij.platform.eel.fs.EelFileSystemApi
|
||||
import com.intellij.platform.eel.fs.EelFileSystemApi.ReplaceExistingDuringMove.DO_NOT_REPLACE_DIRECTORIES
|
||||
import com.intellij.platform.eel.fs.move
|
||||
import com.intellij.platform.eel.fs.EelFileUtils
|
||||
import com.intellij.platform.eel.getOr
|
||||
import com.intellij.platform.eel.provider.asEelPath
|
||||
import com.intellij.platform.eel.provider.asNioPath
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.toEelApi
|
||||
@@ -31,6 +30,7 @@ import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
@@ -114,14 +114,20 @@ internal class CliBasedHatchService private constructor(
|
||||
}
|
||||
|
||||
hatchRuntime.hatchCli().new(projectName, tempDir.asNioPath()).getOr { return it }
|
||||
val target = workingDirectoryPath.asEelPath()
|
||||
eelApi.fs.move(tempDir, target).replaceExisting(DO_NOT_REPLACE_DIRECTORIES).eelIt().getOr { failure ->
|
||||
return Result.failure(FileSystemOperationHatchError(failure.error))
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val source = tempDir.asNioPath()
|
||||
NioFiles.copyRecursively(source, workingDirectoryPath)
|
||||
EelFileUtils.deleteRecursively(source)
|
||||
}
|
||||
}
|
||||
catch (e: IOException) {
|
||||
return Result.failure(FileSystemOperationHatchError(e.localizedMessage ?: e.toString()))
|
||||
}
|
||||
|
||||
return Result.success(ProjectStructure(
|
||||
sourceRoot = target.asNioPath().resolve("src").takeIf { it.isDirectory() },
|
||||
testRoot = target.asNioPath().resolve("tests").takeIf { it.isDirectory() },
|
||||
sourceRoot = workingDirectoryPath.resolve("src").takeIf { it.isDirectory() },
|
||||
testRoot = workingDirectoryPath.resolve("tests").takeIf { it.isDirectory() },
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ package com.jetbrains.python.newProjectWizard
|
||||
import com.intellij.openapi.GitRepositoryInitializer
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.jetbrains.python.PyBundle
|
||||
@@ -17,6 +18,7 @@ import com.jetbrains.python.sdk.configurePythonSdk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Settings each Python project has: [sdkCreator] and [createGitRepository]
|
||||
@@ -33,11 +35,20 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsNotEmptyModuleStructure) {
|
||||
sdkCreator.createPythonModuleStructure(module).getOr { return@coroutineScope it }
|
||||
withBackgroundProgress(project, PyBundle.message("python.sdk.creating.python.module.structure")) {
|
||||
withContext(Dispatchers.IO) {
|
||||
sdkCreator.createPythonModuleStructure(module).also {
|
||||
VfsUtil.markDirtyAndRefresh(false, true, true, baseDir)
|
||||
}
|
||||
}
|
||||
}.getOr { return@coroutineScope it }
|
||||
}
|
||||
val (sdk: Sdk, interpreterStatistics: InterpreterStatisticsInfo) = getSdkAndInterpreter(module)
|
||||
.getOr { return@coroutineScope it }
|
||||
|
||||
val (sdk: Sdk, interpreterStatistics: InterpreterStatisticsInfo) = withBackgroundProgress(project, PyBundle.message("python.sdk.creating.python.sdk")) {
|
||||
getSdkAndInterpreter(module)
|
||||
}.getOr { return@coroutineScope it }
|
||||
|
||||
configurePythonSdk(project, module, sdk)
|
||||
return@coroutineScope Result.success(Pair(sdk, interpreterStatistics))
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.intellij.python.pyproject.model.internal.startAutoImportIfNeeded
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.TraceContext
|
||||
import com.jetbrains.python.errorProcessing.emit
|
||||
import com.jetbrains.python.newProjectWizard.collector.PyProjectTypeGenerator
|
||||
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector.logPythonNewProjectGenerated
|
||||
@@ -70,7 +71,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
|
||||
@RequiresEdt
|
||||
override fun generateProject(project: Project, baseDir: VirtualFile, settings: PyV3BaseProjectSettings, module: Module) {
|
||||
val coroutineScope = project.service<MyService>().coroutineScope
|
||||
coroutineScope.launch {
|
||||
coroutineScope.launch(TraceContext(PyBundle.message("trace.context.new.project.wizard"), coroutineScope)) {
|
||||
generateProjectImpl(settings, module, baseDir)
|
||||
startAutoImportIfNeeded(project)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.intellij.openapi.ui.validation.CHECK_NO_RESERVED_WORDS
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.util.SystemProperties
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.MessageError
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
@@ -36,7 +37,7 @@ class ProjectPathFlows private constructor(val projectPath: Flow<Path?>) {
|
||||
/**
|
||||
* Flow emits project file name only when project path is valid
|
||||
*/
|
||||
val projectName: Flow<@NlsSafe String> = projectPath.filterNotNull().map { it.name.replace(" ", "_") }
|
||||
val projectName: Flow<@NlsSafe String> = projectPath.filterNotNull().map { PyPackageName.normalizeProjectName(it.name) }
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
+7
-6
@@ -28,8 +28,8 @@ import com.jetbrains.python.packaging.PyPackageName
|
||||
import com.jetbrains.python.packaging.PyPackageService
|
||||
import com.jetbrains.python.packaging.PyPackageVersionNormalizer
|
||||
import com.jetbrains.python.packaging.cache.PythonSimpleRepositoryCache
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackageDetails
|
||||
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
|
||||
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
|
||||
@@ -587,10 +587,11 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
|
||||
showNoInterpreterMessage()
|
||||
return
|
||||
}
|
||||
|
||||
serviceScope.launch(Dispatchers.Default) {
|
||||
context.managerUI.reloadPackagesBackground()
|
||||
refreshInstalledPackages()
|
||||
serviceScope.launch(Dispatchers.Default + TraceContext(message("trace.context.packaging.tool.window"), serviceScope)) {
|
||||
withContext(TraceContext(message("trace.context.packaging.tool.window.sdk.reload", context.sdk.name))) {
|
||||
context.managerUI.reloadPackagesBackground()
|
||||
refreshInstalledPackages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,7 +599,7 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
|
||||
val updated = SingleConfigurableEditor(project, PyRepositoriesList(project)).showAndGet()
|
||||
if (updated) {
|
||||
PythonPackagesToolwindowStatisticsCollector.repositoriesChangedEvent.log(project)
|
||||
serviceScope.launch(Dispatchers.IO) {
|
||||
serviceScope.launch(Dispatchers.IO + TraceContext(message("trace.context.packaging.tool.window"), serviceScope)) {
|
||||
val packageService = PyPackageService.getInstance()
|
||||
val repositoryService = service<PyPackageRepositories>()
|
||||
val allRepos = repositoryService.repositories.map { it.repositoryUrl }
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.intellij.python.hatch.HatchVirtualEnvironment
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
@@ -61,7 +62,7 @@ internal class HatchNewEnvironmentCreator<P : PathHolder>(
|
||||
|
||||
val hatchService = module.getHatchService(hatchExecutablePath).getOr { return it }
|
||||
|
||||
val projectStructure = hatchService.createNewProject(module.project.name).getOr { return it }
|
||||
val projectStructure = hatchService.createNewProject(PyPackageName.normalizeProjectName(module.project.name)).getOr { return it }
|
||||
ModuleRootModificationUtil.updateModel(module) { moduleRootModel ->
|
||||
val contentEntry = moduleRootModel.contentEntries.firstOrNull() ?: return@updateModel
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.isSuccess
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.packaging.PyPackage
|
||||
import com.jetbrains.python.packaging.PyPackageName
|
||||
import com.jetbrains.python.packaging.PyRequirement
|
||||
import com.jetbrains.python.packaging.PyRequirementParser
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
@@ -33,6 +34,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
/**
|
||||
@@ -96,6 +98,12 @@ suspend fun setupPoetry(
|
||||
// Build poetry init command with Python version constraint if available
|
||||
val initArgs = mutableListOf("init", "-n")
|
||||
|
||||
val projectName = PyPackageName.normalizeProjectName(projectPath.name)
|
||||
if (projectName.isNotBlank()) {
|
||||
initArgs.add("--name")
|
||||
initArgs.add(projectName)
|
||||
}
|
||||
|
||||
// Validate Python and get version info
|
||||
val pythonInfo = basePythonBinaryPath.validatePythonAndGetInfo().getOr { return it }
|
||||
val major = pythonInfo.languageLevel.majorVersion
|
||||
|
||||
@@ -51,9 +51,10 @@ private class UvLowLevelImpl<P : PathHolder>(private val cwd: Path, private val
|
||||
val initArgs = mutableListOf("init")
|
||||
addPythonArg(initArgs)
|
||||
initArgs.add("--bare")
|
||||
if (cwd.name.isNotBlank()) {
|
||||
val projectName = PyPackageName.normalizeProjectName(cwd.name)
|
||||
if (projectName.isNotBlank()) {
|
||||
initArgs.add("--name")
|
||||
initArgs.add(cwd.name)
|
||||
initArgs.add(projectName)
|
||||
}
|
||||
initArgs.add("--no-project")
|
||||
uvCli.runUv(cwd, null, true, *initArgs.toTypedArray()).getOr { return it }
|
||||
|
||||
Reference in New Issue
Block a user