Programatically activating .pyi typehints

I’m developing a plugin which adds MicroPython support to PyCharm and all other IDEs with the python plugin.

One of the plugin’s features is a built-in MicroPython stub package manager, which will allow users to browse available stub packages, install them and easily activate them in the IDE.

I’m struggling to figure out how sets of .pyi files should properly be attached and detached programatically. I’m currently relying on a very fragile approach of modifying the projectLibraryTable. This doesn’t always work and I’m aware that it isn’t best practice.

I have tried using the AdditionalLibraryRootsProvider:

class MpyAdditionalLibraryRootsProvider : AdditionalLibraryRootsProvider() {
    override fun getAdditionalProjectLibraries(project: Project): Collection<SyntheticLibrary> {
        val settings = project.service<MpySettingsService>()
        val pythonService = project.service<MpyPythonService>()

        if (!settings.state.areStubsEnabled || settings.state.activeStubsPackage.isNullOrBlank()) {
            println("return 1")
            return emptyList()
        }

        val availableStubs = pythonService.getAvailableStubs()
        val activeStubPackage = settings.state.activeStubsPackage

        if (!availableStubs.contains(activeStubPackage) || activeStubPackage == null) {
            println("return 2")
            return emptyList()
        }

        val rootPath = "${MpyPythonService.stubsPath}/$activeStubPackage"
        val virtualFile = LocalFileSystem.getInstance().findFileByPath(rootPath)

        if (virtualFile == null) {
            println("return 3")
            return emptyList()
        }

        println("Attaching stub package: $virtualFile")

        return listOf(
            SyntheticLibrary.newImmutableLibrary(
                listOf(virtualFile),
                emptyList(),
                emptySet(),
                null
            )
        )
    }
}
fun notifyStubsChanged(project: Project, oldStubPackage: String, newStubPackage: String) {
    val settings = project.service<MpySettingsService>()
    val pythonService = project.service<MpyPythonService>()

    val oldRoots = if (pythonService.getAvailableStubs().contains(oldStubPackage)) {
        println("got into old condition")
        val oldVirtualFile = LocalFileSystem.getInstance().findFileByPath("${MpyPythonService.stubsPath}/$oldStubPackage")
        println(oldVirtualFile)
        if (oldVirtualFile != null) listOf(oldVirtualFile) else emptyList()
    } else emptyList()

    val stubsEnabled = settings.state.areStubsEnabled

    val newRoots = if (stubsEnabled && pythonService.getAvailableStubs().contains(newStubPackage)) {
        println("got into new condition")
        val newVirtualFile = LocalFileSystem.getInstance().findFileByPath("${MpyPythonService.stubsPath}/$newStubPackage")
        println(newVirtualFile)
        if (newVirtualFile != null) listOf(newVirtualFile) else emptyList()
    } else emptyList()

    println("Removing old roots: $oldRoots")
    println("Adding new roots: $newRoots")

    ApplicationManager.getApplication().runWriteAction {
        AdditionalLibraryRootsListener.fireAdditionalLibraryChanged(
            project,
            "MicroPython Stubs",
            oldRoots,
            newRoots,
            "MicroPythonLibraryProvider"
        )
    }
}

I can see the debug prints activating the correct folders, however, the interpreter still doesn’t recognize the MicroPython specific modules when imported. The same stub packages that I’m attempting to attach here work when I attach them by modifying the projectLibraryTable.

Could someone please help me figure out what the correct way to programatically attach/detach .pyi stub packages is?

Hi there! Have you checked how .pyi stubs for different boards are enabled through FacetLibraryConfigurator in intellij-mircopython plugin (intellij-micropython/src/main/kotlin/com/jetbrains/micropython/settings/MicroPythonFacet.kt at c17598d4d09210e077f95d7b19a961d872f2e36c · JetBrains/intellij-micropython · GitHub)

Hello, thank you for the response!

I have checked how that plugin handles it, in fact, my plugin is a fork of it.

The FacetLibraryConfigurator approach did not work. It struggled to successfully attach the stubs and make them recognized, and the removeLibrary() method did nothing.

I have looked at what the FacetLibraryConfigurator library methods do internally, and with a lot of trial and error managed to piece together something that mostly works. It can sometime fail to attach the library if it’s called shortly after IDE start-up and it does not work in IntelliJ.

I’m aware that the code isn’t ideal, I wrote it when I started with Kotlin and plugin development. The library shouldn’t be re-created each time, it should be re-used if it’s already there, those are changes I’d make if I can get it working in IntelliJ.

  fun updateLibrary() {
      val activeStubPackage = settings.state.activeStubsPackage
      val availableStubs = getAvailableStubs()

      DumbService.getInstance(project).smartInvokeLater {
          ApplicationManager.getApplication().runWriteAction {
              val projectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(project)
              val projectLibraryModel = projectLibraryTable.modifiableModel

              for (library in projectLibraryModel.libraries) {
                  if (library.name == LIBRARY_NAME) {
                      projectLibraryTable.removeLibrary(library)
                      projectLibraryModel.removeLibrary(library)
                  }
              }

              if (settings.state.areStubsEnabled &&
                  !activeStubPackage.isNullOrBlank() &&
                  availableStubs.contains(activeStubPackage)
              ) {
                  val newLibrary =
                      projectLibraryModel.createLibrary(LIBRARY_NAME, PythonLibraryType.getInstance().kind)
                  val newModel = newLibrary.modifiableModel

                  val rootUrl = "$stubsPath/$activeStubPackage"
                  val virtualFile = LocalFileSystem.getInstance().findFileByPath(rootUrl)

                  newModel.addRoot(virtualFile!!, OrderRootType.CLASSES)

                  for (module in ModuleManager.getInstance(project).modules) {
                      val moduleModel = ModifiableModelsProvider.getInstance().getModuleModifiableModel(module)
                      moduleModel.addLibraryEntry(newLibrary)

                      newModel.commit()
                      projectLibraryModel.commit()
                      ModifiableModelsProvider.getInstance().commitModuleModifiableModel(moduleModel)
                  }
              } else {
                  projectLibraryModel.commit()
              }
          }
      }
  }

The goal is to have an approach that will work in all IDEs that have the python plugin and for it to work in a way where the libraries stay attached and persist between IDE restarts.

Bump - I still haven’t been able to figure out how to make this work for IntelliJ

I’ve finally been able to make IntelliJ recognize the attached stub package libraries by rewriting the procedure and changing the order of steps of how the library is attached.