PY-27360 Unify implementations of PyPI and Conda package caches

This commit is contained in:
Mikhail Golubev
2017-12-14 21:29:12 +03:00
parent e93970a179
commit dba31cf0d8
6 changed files with 181 additions and 199 deletions
@@ -1,112 +0,0 @@
// 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.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
/**
* @author Mikhail Golubev
*/
public class CondaPackageCache {
private static final Logger LOG = Logger.getInstance(PyPIPackageCache.class);
private static final Gson ourGson = new GsonBuilder().create();
private static CondaPackageCache ourInstance = null;
@NotNull
public static synchronized CondaPackageCache getInstance() {
if (ourInstance == null) {
ourInstance = new CondaPackageCache();
try (Reader reader = Files.newBufferedReader(getCachePath(), StandardCharsets.UTF_8)) {
ourInstance = ourGson.fromJson(reader, CondaPackageCache.class);
LOG.info("Loaded " + ourInstance.myPackages.size() + " packages from " + getCachePath());
}
catch (IOException exception) {
LOG.warn("Cannot load Conda package cache from the filesystem", exception);
}
}
return ourInstance;
}
@NotNull
public static synchronized CondaPackageCache reload(@NotNull Map<String, List<String>> packageNames) {
ourInstance = new CondaPackageCache(packageNames);
try {
Files.createDirectories(getCachePath().getParent());
try (Writer writer = Files.newBufferedWriter(getCachePath(), StandardCharsets.UTF_8)) {
ourGson.toJson(ourInstance, writer);
}
}
catch (IOException exception) {
LOG.warn("Cannot save Conda package cache to the filesystem", exception);
}
return ourInstance;
}
@NotNull
private static Path getCachePath() {
return Paths.get(PathManager.getSystemPath(), "python_packages", "conda-cache.json");
}
public CondaPackageCache() {
}
private CondaPackageCache(@NotNull Map<String, List<String>> nameToVersion) {
for (Map.Entry<String, List<String>> entry : nameToVersion.entrySet()) {
myPackages.put(entry.getKey(), new PackageInfo(entry.getValue()));
}
}
public boolean containsPackage(@NotNull String packageName) {
return myPackages.containsKey(packageName);
}
@NotNull
public Set<String> getPackageNames() {
return Collections.unmodifiableSet(myPackages.keySet());
}
/**
* Returns available package versions sorted in the reversed order using {@link com.intellij.webcore.packaging.PackageVersionComparator}.
* @param packageName case-insensitive name of a package
*/
@NotNull
public List<String> getVersions(@NotNull String packageName) {
return Collections.unmodifiableList(myPackages.get(packageName).getVersions());
}
@SerializedName("packages")
private final TreeMap<String, PackageInfo> myPackages = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private static class PackageInfo {
@SerializedName("v")
private List<String> myVersions;
public PackageInfo(@NotNull List<String> versions) {
myVersions = versions;
}
@SuppressWarnings("unused")
public PackageInfo() {
}
@NotNull
public List<String> getVersions() {
return Collections.unmodifiableList(myVersions);
}
}
}
@@ -0,0 +1,125 @@
// 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.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
/**
* @author Mikhail Golubev
*/
public abstract class PyAbstractPackageCache {
private static final Logger LOG = Logger.getInstance(PyPIPackageCache.class);
private static final Gson ourGson = new GsonBuilder().create();
@SerializedName("packages")
protected TreeMap<String, PackageInfo> myPackages = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
protected PyAbstractPackageCache() {
}
@NotNull
protected static <T extends PyAbstractPackageCache> T load(@NotNull Class<T> classToken,
@NotNull T fallbackValue,
@NotNull String cacheFileName) {
T cache = fallbackValue;
try (Reader reader = Files.newBufferedReader(getCachePath(cacheFileName), StandardCharsets.UTF_8)) {
cache = ourGson.fromJson(reader, classToken);
LOG.info("Loaded " + cache.getPackageNames().size() + " packages from " + getCachePath(cacheFileName));
}
catch (IOException exception) {
LOG.warn("Cannot load " + cacheFileName + " package cache from the filesystem", exception);
}
return cache;
}
protected static void store(@NotNull PyAbstractPackageCache newValue, @NotNull String cacheFileName) {
try {
final Path cacheFilePath = getCachePath(cacheFileName);
Files.createDirectories(cacheFilePath.getParent());
try (Writer writer = Files.newBufferedWriter(cacheFilePath, StandardCharsets.UTF_8)) {
ourGson.toJson(newValue, writer);
}
}
catch (IOException exception) {
LOG.warn("Cannot save " + cacheFileName + " package cache to the filesystem", exception);
}
}
@NotNull
private static Path getCachePath(@NotNull String cacheFileName) {
return Paths.get(PathManager.getSystemPath(), "python_packages", cacheFileName);
}
/**
* Returns a case-insensitive set of packages names available in the cache.
*/
@NotNull
public Set<String> getPackageNames() {
return Collections.unmodifiableSet(myPackages.keySet());
}
/**
* Checks that the given name is among those available in the repository <em>case-insensitively</em>.
* <p>
* Note that if the cache hasn't been initialized yet or there was an error during its loading,
* {@link #load(Class, PyAbstractPackageCache, String)} returns an empty sentinel value, and, therefore, this method will return {@code false}.
* It's worth writing code analysis so that this value doesn't lead to false positives in the editor
* when the cache is merely not ready.
*
* @param name case-insensitive name of a package
*/
public boolean containsPackage(@NotNull String name) {
return myPackages.containsKey(name);
}
/**
* Returns available package versions sorted in the reversed order using
* {@link com.intellij.webcore.packaging.PackageVersionComparator} so that the latest version is the first on the list
* or {@code null} if the given package is not contained in the cache or this feature is not available.
*
* @param packageName case-insensitive name of a package
*/
@Nullable
public List<String> getVersions(@NotNull String packageName) {
final PackageInfo packageInfo = myPackages.get(packageName);
return packageInfo != null ? packageInfo.getVersions() : null;
}
protected static class PackageInfo {
public static final PackageInfo EMPTY = new PackageInfo();
@SerializedName("v")
private List<String> myVersions;
public PackageInfo(@NotNull List<String> versions) {
myVersions = versions;
}
@SuppressWarnings("unused")
public PackageInfo() {
}
@Nullable
public List<String> getVersions() {
return myVersions != null ? Collections.unmodifiableList(myVersions) : null;
}
}
}
@@ -0,0 +1,40 @@
// 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 org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
/**
* @author Mikhail Golubev
*/
public class PyCondaPackageCache extends PyAbstractPackageCache {
private static final String CACHE_FILE_NAME = "conda-cache.json";
private static PyCondaPackageCache ourInstance;
@NotNull
public static synchronized PyCondaPackageCache getInstance() {
if (ourInstance == null) {
ourInstance = load(PyCondaPackageCache.class, new PyCondaPackageCache(), CACHE_FILE_NAME);
}
return ourInstance;
}
@NotNull
public static synchronized PyCondaPackageCache reload(@NotNull Map<String, List<String>> packageNames) {
ourInstance = new PyCondaPackageCache(packageNames);
store(ourInstance, CACHE_FILE_NAME);
return ourInstance;
}
private PyCondaPackageCache() {
}
private PyCondaPackageCache(@NotNull Map<String, List<String>> nameToVersion) {
for (Map.Entry<String, List<String>> entry : nameToVersion.entrySet()) {
myPackages.put(entry.getKey(), new PyAbstractPackageCache.PackageInfo(entry.getValue()));
}
}
}
@@ -58,7 +58,7 @@ public class PyCondaPackageService implements PersistentStateComponent<PyCondaPa
}
public void loadAndGetPackages(boolean force) {
if (CondaPackageCache.getInstance().getPackageNames().isEmpty() || force) {
if (PyCondaPackageCache.getInstance().getPackageNames().isEmpty() || force) {
updatePackagesCache();
}
}
@@ -194,13 +194,13 @@ public class PyCondaPackageService implements PersistentStateComponent<PyCondaPa
versions.add(-(sortedIndex + 1), packageVersion);
}
}
CondaPackageCache.reload(nameToVersions);
PyCondaPackageCache.reload(nameToVersions);
LAST_TIME_CHECKED = System.currentTimeMillis();
}
@NotNull
public List<String> getPackageVersions(@NotNull final String packageName) {
return CondaPackageCache.getInstance().getVersions(packageName);
return ContainerUtil.notNullize(PyCondaPackageCache.getInstance().getVersions(packageName));
}
public void updateChannels() {
@@ -1,45 +1,22 @@
// 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.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @author Mikhail Golubev
*/
public class PyPIPackageCache {
private static final Logger LOG = Logger.getInstance(PyPIPackageCache.class);
private static final Gson ourGson = new GsonBuilder().create();
private static PyPIPackageCache ourInstance = null;
public class PyPIPackageCache extends PyAbstractPackageCache {
private static final String CACHE_FILE_NAME = "pypi-cache.json";
private static PyPIPackageCache ourInstance;
@NotNull
public static synchronized PyPIPackageCache getInstance() {
if (ourInstance == null) {
ourInstance = new PyPIPackageCache();
try (Reader reader = Files.newBufferedReader(getCachePath(), StandardCharsets.UTF_8)) {
ourInstance = ourGson.fromJson(reader, PyPIPackageCache.class);
LOG.info("Loaded " + ourInstance.getPackageNames().size() + " packages from " + getCachePath());
}
catch (IOException exception) {
LOG.warn("Cannot load PyPI package cache from the filesystem", exception);
}
ourInstance = PyAbstractPackageCache.load(PyPIPackageCache.class, new PyPIPackageCache(), CACHE_FILE_NAME);
}
return ourInstance;
}
@@ -47,67 +24,19 @@ public class PyPIPackageCache {
@NotNull
public static synchronized PyPIPackageCache reload(@NotNull List<String> packageNames) {
ourInstance = new PyPIPackageCache(packageNames);
try {
Files.createDirectories(getCachePath().getParent());
try (Writer writer = Files.newBufferedWriter(getCachePath(), StandardCharsets.UTF_8)) {
ourGson.toJson(ourInstance, writer);
}
}
catch (IOException exception) {
LOG.warn("Cannot save PyPI package cache to the filesystem", exception);
}
store(ourInstance, CACHE_FILE_NAME);
return ourInstance;
}
// For GSON
private PyPIPackageCache() {
}
public PyPIPackageCache(@NotNull List<String> packageNames) {
myPackageNames = ContainerUtil.map(packageNames, PackageInfo::new);
}
@NotNull
private static Path getCachePath() {
return Paths.get(PathManager.getSystemPath(), "python_packages", "pypi-cache.json");
}
@SerializedName("packages")
private List<PackageInfo> myPackageNames = new ArrayList<>();
@NotNull
public List<String> getPackageNames() {
return Collections.unmodifiableList(ContainerUtil.map(myPackageNames, PackageInfo::getName));
}
/**
* Checks that the given name is among those available at PyPI <em>case-insensitively</em>.
* <p>
* Note that if the cache hasn't been initialized yet or there was an error during its loading,
* {@link #getInstance()} returns an empty sentinel value, and, therefore, this method will return {@code false}.
* It's worth writing code analysis so that this value doesn't lead to false positives in the editor
* when the cache is merely not ready.
*/
public boolean containsPackage(@NotNull String name) {
final Comparator<PackageInfo> caseInsensitiveNameComparator = Comparator.comparing(PackageInfo::getName, String.CASE_INSENSITIVE_ORDER);
return Collections.binarySearch(myPackageNames, new PackageInfo(name), caseInsensitiveNameComparator) >= 0;
}
private static class PackageInfo {
@SerializedName("n")
private String myName;
@SuppressWarnings("unused")
public PackageInfo() {
}
public PackageInfo(@NotNull String name) {
myName = name;
}
@NotNull
public String getName() {
return myName;
for (String name : packageNames) {
// Don't save null as a value, since GSON excludes such entries from serialization,
// unless GsonBuilder#serializeNulls() is used, but then object fields with null values
// are saved as well.
myPackages.put(name, PackageInfo.EMPTY);
}
}
}
@@ -27,7 +27,7 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.CatchingConsumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.webcore.packaging.RepoPackage;
import com.jetbrains.python.packaging.CondaPackageCache;
import com.jetbrains.python.packaging.PyCondaPackageCache;
import com.jetbrains.python.packaging.PyCondaPackageManagerImpl;
import com.jetbrains.python.packaging.PyCondaPackageService;
import com.jetbrains.python.packaging.PyPackageManager;
@@ -153,7 +153,7 @@ public class PyCondaManagementService extends PyPackageManagementService {
@NotNull
private static List<RepoPackage> getCachedCondaPackages() {
final CondaPackageCache instance = CondaPackageCache.getInstance();
final PyCondaPackageCache instance = PyCondaPackageCache.getInstance();
return ContainerUtil.map(instance.getPackageNames(), name -> {
final String latestVersion = ContainerUtil.getFirstItem(instance.getVersions(name));
return new RepoPackage(name, null, latestVersion);