Merge branch 'python-skeletons'

This commit is contained in:
Andrey Vlasovskikh
2013-09-02 19:12:45 +04:00
7 changed files with 991 additions and 10 deletions
+139
View File
@@ -0,0 +1,139 @@
Python Skeletons
================
_This proposal is a draft._
Python skeletons are Python files that contain API definitions of existing
libraries extended for static analysis tools.
Rationale
---------
Python is a dynamic language less suitable for static code analysis than static
languages like C or Java. Although Python static analysis tools can extract
some information from Python source code without executing it, this information
is often very shallow and incomplete.
Dynamic features of Python are very useful for user code. But using these
features in APIs of third-party libraries and the standard library is not
always a good idea. Tools (and users, in fact) need clear definitions of APIs.
Often library API definitions are quite static and easy to grasp (defined
using `class`, `def`), but types of function parameters and return values
usually are not specified. Sometimes API definitions involve metaprogramming.
As there is not enough information in API definition code of libraries,
developers of static analysis tools collect extended API data themselves and
store it in their own formats. For example, PyLint uses imperative AST
transformations of API modules in order to extend them with hard-coded data.
PyCharm extends APIs via its proprietary database of declarative type
annotations. The absence of a common extended API information format makes it
hard for developers and users of tools to collect and share data.
Proposal
--------
The proposal is to create a common database of extended API definitions as a
collection of Python files called skeletons. Static analysis tools already
understand Python code, so it should be easy to start extracting API
definitions from these Python skeleton files. Regular function and class
definitions can be extended with additional docstrings and decorators, e.g. for
providing types of function parameters and return values. Static analysis tools
may use a subset of information contained in skeleton files needed for their
operation. Using Python files instead of a custom API definition format will
also make it easier for users to populate the skeletons database.
Declarative Python API definitions for static analysis tools cannot cover all
dynamic tricks used in real APIs of libraries: some of them still require
library-specific code analysis. Nevertheless the skeletons database is enough
for many libraries.
The proposed [python-skeletons](https://github.com/JetBrains/python-skeletons)
repository is hosted on GitHub.
Conventions
-----------
Skeletons should respect PEP-8 and PEP-257 style guides.
The most simple way of specifying types in skeletons is Sphinx docstrings.
Function annotations could be used for specifying types, but they are
available only for Python 3.
There is no standard notation for specifying types in Python code. We propose
the following notation:
Foo # Class Foo visible in the current scope
x.y.Bar # Class Bar from x.y module
Foo | Bar # Foo or Bar
(Foo, Bar) # Tuple of Foo and Bar
list[Foo] # List of Foo elements
dict[Foo, Bar] # Dict from Foo to Bar
T # Generic type (T-Z are reserved for generics)
T <= Foo # Generic type with upper bound Foo
Foo[T] # Foo parameterized with T
(Foo, Bar) -> Baz # Function of Foo and Bar that returns Baz
The formal syntax is defined in `pytypes` library (work in progress).
There are several shortcuts available:
unknown # Unknown type
None # type(None)
string # Py2: str | unicode, Py3: str
bytestring # Py2: str | unicode, Py3: bytes
bytes # Py2: str, Py3: bytes
unicode # Py2: unicode, Py3: str
The syntax is a subject to change. It is almost compatible to Python (except
function types), but its semantics differs from Python (no `|`, no implicitly
visible names, no generic types). So you cannot use these expressions in
Python 3 function annotations. See also `python-righarrow`, `typeannotations`.
The recommended way of checking the version of Python is:
import sys
if sys.version_info >= (2, 7) and sys.version_info < (3,):
def from_27_until_30():
pass
PyCharm
-------
PyCharm 3 can extract the following information from the skeletons:
* Parameters of functions and methods
* Return types and parameter types of functions and methods
* Types of assignment targets
* Extra module members
* TODO
PyCharm 3 comes with a snapshot of the Python skeletons repository. You
should not modify it, because it will be updated with the PyCharm
installation. If you want to change the skeletons, clone the skeletons GitHub
repository into your PyCharm config directory:
cd <PyCharm config>
git clone https://github.com/JetBrains/python-skeletons.git
where `<PyCharm config>` is:
* Mac OS X: `~/Library/Preferences/PyCharmXX/config`
* Linux: `~/.PyCharmXX/config`
* Windows: `<User home>\.PyCharmXX\config`
Please send your PyCharm-related bug reports and feature requests to
[PyCharm issue tracker](http://youtrack.jetbrains.com/issues/PY).
Feedback
--------
If you want to contribute, send your pull requests to the Python skeletons
repository on GitHub. Please make sure, that you follow the conventions above.
Use [code-quality](http://mail.python.org/mailman/listinfo/code-quality)
mailing list to discuss Python skeletons.
@@ -0,0 +1,377 @@
"""Skeletons for built-in symbols."""
import sys as __sys
def abs(number):
"""Return the absolute value of the argument.
:type number: T
:rtype: T | unknown
"""
pass
def all(iterable):
"""Return True if bool(x) is True for all values x in the iterable.
:type iterable: collections.Iterable
:rtype: bool
"""
pass
def any(iterable):
"""Return True if bool(x) is True for any x in the iterable.
:type iterable: collections.Iterable
:rtype: bool
"""
pass
def bin(number):
"""Return the binary representation of an integer or long integer.
:type number: numbers.Number
:rtype: bytes
"""
pass
def callable(object):
"""Return whether the object is callable (i.e., some kind of function).
Note that classes are callable, as are instances with a __call__() method.
:rtype: bool
"""
pass
def chr(i):
"""Return a string of one character with ordinal i; 0 <= i < 256.
:type i: int
:rtype: string
"""
pass
def cmp(x, y):
"""Return negative if x<y, zero if x==y, positive if x>y.
:rtype: int
"""
pass
def dir(object=None):
"""If called without an argument, return the names in the current scope.
Else, return an alphabetized list of names comprising (some of) the attributes
of the given object, and of attributes reachable from it.
:rtype: list[string]
"""
pass
def divmod(x, y):
"""Return the tuple ((x-x%y)/y, x%y).
:type x: numbers.Number
:type y: numbers.Number
:rtype: (int | long | float | unknown, int | long | float | unknown)
"""
pass
def filter(function_or_none, sequence):
"""Return those items of sequence for which function(item) is true. If
function is None, return the items that are true. If sequence is a tuple
or string, return the same type, else return a list.
:type function_or_none: collections.Callable | None
:type sequence: T <= list | collections.Iterable | bytes | unicode
:rtype: T
"""
pass
def getattr(object, name, default=None):
"""Get a named attribute from an object; getattr(x, 'y') is equivalent to
x.y. When a default argument is given, it is returned when the attribute
doesn't exist; without it, an exception is raised in that case.
:type name: string
:rtype: object | unknown
"""
pass
def globals():
"""Return the dictionary containing the current scope's global variables.
:rtype: dict[string, unknown]
"""
pass
def hasattr(object, name):
"""Return whether the object has an attribute with the given name.
:type name: string
:rtype: bool
"""
pass
def hash(object):
"""Return a hash value for the object.
:rtype: int
"""
pass
def hex(number):
"""Return the hexadecimal representation of an integer or long integer.
:type number: numbers.Integral
:rtype: string
"""
pass
def id(object):
"""Return the identity of an object.
:rtype: int
"""
pass
def isinstance(object, class_or_type_or_tuple):
"""Return whether an object is an instance of a class or of a subclass
thereof.
:rtype: bool
"""
pass
def issubclass(C, B):
"""Return whether class C is a subclass (i.e., a derived class) of class B.
:rtype: bool
"""
pass
def iter(source, sentinel=None):
"""Get an iterator from an object. In the first form, the argument must
supply its own iterator, or be a sequence. In the second form, the callable
is called until it returns the sentinel.
:type source: collections.Iterable[T]
:rtype: collections.Iterator[T]
"""
pass
def len(object):
"""Return the number of items of a sequence or mapping.
:type object: collections.Sized
:rtype: int
"""
pass
def locals():
"""Update and return a dictionary containing the current scope's local
variables.
:rtype: dict[string, unknown]
"""
pass
def map(function, sequence, *sequence_1):
"""Return a list of the results of applying the function to the items of
the argument sequence(s).
:type function: ((T) -> V) | None
:type sequence: collections.Iterable[T]
:rtype: list[V] | bytes | unicode
"""
pass
def next(iterator, default=None):
"""Return the next item from the iterator.
:type iterator: collections.Iterator[T]
:rtype: T
"""
pass
def oct(number):
"""Return the octal representation of an integer or long integer.
:type number: numbers.Integral
:rtype: string
"""
pass
def open(name, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=None, opener=None):
"""Open a file, returns a file object.
:type name: string
:type mode: string
:type buffering: int
:type encoding: string | None
:type errors: string | None
:rtype: file
"""
pass
def ord(c):
"""Return the integer ordinal of a one-character string.
:type c: string
:rtype: int
"""
pass
def pow(x, y, z=None):
"""With two arguments, equivalent to x**y. With three arguments,
equivalent to (x**y) % z, but may be more efficient (e.g. for longs).
:type x: numbers.Number
:type y: numbers.Number
:type z: numbers.Number | None
:rtype: int | long | float | complex
"""
pass
if __sys.version_info < (3,):
def range(start, stop=None, step=None):
"""Return a list containing an arithmetic progression of integers.
:type start: numbers.Integral
:type stop: numbers.Integral | None
:type step: numbers.Integral | None
:rtype: list[int]
"""
pass
def reduce(function, sequence, initial=None):
"""Apply a function of two arguments cumulatively to the items of a sequence,
from left to right, so as to reduce the sequence to a single value.
:type function: collections.Callable
:type sequence: collections.Iterable
:type initial: T
:rtype: T | unknown
"""
pass
def repr(object):
"""
Return the canonical string representation of the object.
:rtype: string
"""
pass
def round(number, ndigits=None):
"""Round a number to a given precision in decimal digits (default 0 digits).
:type number: numbers.Real
:type ndigits: numbers.Real | None
:rtype: float
"""
pass
class slice(object):
def __init__(self, start, stop=None, step=None):
"""Create a slice object. This is used for extended slicing (e.g. a[0:10:2]).
:type start: numbers.Integral
:type stop: numbers.Integral | None
:type step: numbers.Integral | None
"""
return
def vars(object=None):
"""Without arguments, equivalent to locals(). With an argument, equivalent
to object.__dict__.
:rtype: dict[string, unknown]
"""
pass
class object:
""" The most base type."""
@staticmethod
def __new__(cls, *more):
"""Create a new object.
:type cls: T
:rtype: T
"""
pass
class enumerate(object):
"""enumerate object."""
def __init__(self, iterable, start=0):
"""Create an enumerate object.
:type iterable: collections.Iterable[T]
:type start: int | long
:rtype: enumerate[int, T]
"""
pass
def next(self):
"""Return the next value, or raise StopIteration.
:rtype: (int, T)
"""
pass
def __iter__(self):
"""x.__iter__() <==> iter(x).
:rtype: enumerate[int, T]
"""
pass
if __sys.version_info < (3,):
class xrange(object):
"""xrange object."""
def __init__(self, start, stop=None, step=None):
"""Create an xrange object.
:type start: numbers.Integral
:type stop: numbers.Integral | None
:type step: numbers.Integral | None
:rtype: xrange[int]
"""
pass
@@ -0,0 +1,177 @@
"""Skeleton for 'nose.tools' module."""
import sys
def assert_equal(first, second, msg=None):
"""Fail if the two objects are unequal as determined by the '==' operator.
"""
pass
def assert_not_equal(first, second, msg=None):
"""Fail if the two objects are equal as determined by the '==' operator.
"""
pass
def assert_true(expr, msg=None):
"""Check that the expression is true."""
pass
def assert_false(expr, msg=None):
"""Check that the expression is false."""
pass
if sys.version_info >= (2, 7):
def assert_is(expr1, expr2, msg=None):
"""Just like assert_true(a is b), but with a nicer default message."""
pass
def assert_is_not(expr1, expr2, msg=None):
"""Just like assert_true(a is not b), but with a nicer default message.
"""
pass
def assert_is_none(obj, msg=None):
"""Same as assert_true(obj is None), with a nicer default message.
"""
pass
def assert_is_not_none(obj, msg=None):
"""Included for symmetry with assert_is_none."""
pass
def assert_in(member, container, msg=None):
"""Just like assert_true(a in b), but with a nicer default message."""
pass
def assert_not_in(member, container, msg=None):
"""Just like assert_true(a not in b), but with a nicer default message.
"""
pass
def assert_is_instance(obj, cls, msg=None):
"""Same as assert_true(isinstance(obj, cls)), with a nicer default
message.
"""
pass
def assert_not_is_instance(obj, cls, msg=None):
"""Included for symmetry with assert_is_instance."""
pass
def assert_raises(excClass, callableObj=None, *args, **kwargs):
"""Fail unless an exception of class excClass is thrown by callableObj when
invoked with arguments args and keyword arguments kwargs.
If called with callableObj omitted or None, will return a
context object used like this::
with assert_raises(SomeException):
do_something()
:rtype: unittest.case._AssertRaisesContext | None
"""
pass
if sys.version_info >= (2, 7):
def assert_raises_regexp(expected_exception, expected_regexp,
callable_obj=None, *args, **kwargs):
"""Asserts that the message in a raised exception matches a regexp.
:rtype: unittest.case._AssertRaisesContext | None
"""
pass
def assert_almost_equal(first, second, places=None, msg=None, delta=None):
"""Fail if the two objects are unequal as determined by their difference
rounded to the given number of decimal places (default 7) and comparing to
zero, or by comparing that the between the two objects is more than the
given delta.
"""
pass
def assert_not_almost_equal(first, second, places=None, msg=None, delta=None):
"""Fail if the two objects are equal as determined by their difference
rounded to the given number of decimal places (default 7) and comparing to
zero, or by comparing that the between the two objects is less than the
given delta.
"""
pass
if sys.version_info >= (2, 7):
def assert_greater(a, b, msg=None):
"""Just like assert_true(a > b), but with a nicer default message."""
pass
def assert_greater_equal(a, b, msg=None):
"""Just like assert_true(a >= b), but with a nicer default message."""
pass
def assert_less(a, b, msg=None):
"""Just like assert_true(a < b), but with a nicer default message."""
pass
def assert_less_equal(a, b, msg=None):
"""Just like self.assertTrue(a <= b), but with a nicer default
message.
"""
pass
def assert_regexp_matches(text, expected_regexp, msg=None):
"""Fail the test unless the text matches the regular expression."""
pass
def assert_not_regexp_matches(text, unexpected_regexp, msg=None):
"""Fail the test if the text matches the regular expression."""
pass
def assert_items_equal(expected_seq, actual_seq, msg=None):
"""An unordered sequence specific comparison. It asserts that
actual_seq and expected_seq have the same element counts.
"""
pass
def assert_dict_contains_subset(expected, actual, msg=None):
"""Checks whether actual is a superset of expected."""
pass
def assert_multi_line_equal(first, second, msg=None):
"""Assert that two multi-line strings are equal."""
pass
def assert_sequence_equal(seq1, seq2, msg=None, seq_type=None):
"""An equality assertion for ordered sequences (like lists and tuples).
"""
pass
def assert_list_equal(list1, list2, msg=None):
"""A list-specific equality assertion."""
pass
def assert_tuple_equal(tuple1, tuple2, msg=None):
"""A tuple-specific equality assertion."""
pass
def assert_set_equal(set1, set2, msg=None):
"""A set-specific equality assertion."""
pass
def assert_dict_equal(d1, d2, msg=None):
"""A dict-specific equality assertion."""
pass
assert_equals = assert_equal
assert_not_equals = assert_not_equal
assert_almost_equals = assert_almost_equal
assert_not_almost_equals = assert_not_almost_equal
+277
View File
@@ -0,0 +1,277 @@
"""Skeleton for 're' stdlib module."""
def compile(pattern, flags=0):
"""Compile a regular expression pattern, returning a pattern object.
:type pattern: bytes | unicode
:type flags: int
:rtype: __Regex
"""
pass
def search(pattern, string, flags=0):
"""Scan through string looking for a match, and return a corresponding
match instance. Return None if no position in the string matches.
:type pattern: bytes | unicode | __Regex
:type string: T <= bytes | unicode
:type flags: int
:rtype: __Match[T] | None
"""
pass
def match(pattern, string, flags=0):
"""Matches zero or more characters at the beginning of the string.
:type pattern: bytes | unicode | __Regex
:type string: T <= bytes | unicode
:type flags: int
:rtype: __Match[T] | None
"""
pass
def split(pattern, string, maxsplit=0, flags=0):
"""Split string by the occurrences of pattern.
:type pattern: bytes | unicode | __Regex
:type string: T <= bytes | unicode
:type maxsplit: int
:type flags: int
:rtype: list[T]
"""
pass
def findall(pattern, string, flags=0):
"""Return a list of all non-overlapping matches of pattern in string.
:type pattern: bytes | unicode | __Regex
:type string: T <= bytes | unicode
:type flags: int
:rtype: list[T]
"""
pass
def finditer(pattern, string, flags=0):
"""Return an iterator over all non-overlapping matches for the pattern in
string. For each match, the iterator returns a match object.
:type pattern: bytes | unicode | __Regex
:type string: T <= bytes | unicode
:type flags: int
:rtype: collections.Iterable[__Match[T]]
"""
pass
def sub(pattern, repl, string, count=0, flags=0):
"""Return the string obtained by replacing the leftmost non-overlapping
occurrences of pattern in string by the replacement repl.
:type pattern: bytes | unicode | __Regex
:type repl: bytes | unicode | collections.Callable
:type string: T <= bytes | unicode
:type count: int
:type flags: int
:rtype: T
"""
pass
def subn(pattern, repl, string, count=0, flags=0):
"""Return the tuple (new_string, number_of_subs_made) found by replacing
the leftmost non-overlapping occurrences of pattern with the
replacement repl.
:type pattern: bytes | unicode | __Regex
:type repl: bytes | unicode | collections.Callable
:type string: T <= bytes | unicode
:type count: int
:type flags: int
:rtype: (T, int)
"""
pass
def escape(string):
"""Escape all the characters in pattern except ASCII letters and numbers.
:type string: T <= bytes | unicode
:type: T
"""
pass
class __Regex(object):
"""Mock class for a regular expression pattern object."""
def __init__(self, flags, groups, groupindex, pattern):
"""Create a new pattern object.
:type flags: int
:type groups: int
:type groupindex: dict[bytes | unicode, int]
:type pattern: bytes | unicode
"""
self.flags = flags
self.groups = groups
self.groupindex = groupindex
self.pattern = pattern
def search(self, string, pos=0, endpos=-1):
"""Scan through string looking for a match, and return a corresponding
match instance. Return None if no position in the string matches.
:type string: T <= bytes | unicode
:type pos: int
:type endpos: int
:rtype: __Match[T] | None
"""
pass
def match(self, string, pos=0, endpos=-1):
"""Matches zero | more characters at the beginning of the string.
:type string: T <= bytes | unicode
:type pos: int
:type endpos: int
:rtype: __Match[T] | None
"""
pass
def split(self, string, maxsplit=0):
"""Split string by the occurrences of pattern.
:type string: T <= bytes | unicode
:type maxsplit: int
:rtype: list[T]
"""
pass
def findall(self, string, pos=0, endpos=-1):
"""Return a list of all non-overlapping matches of pattern in string.
:type string: T <= bytes | unicode
:type pos: int
:type endpos: int
:rtype: list[T]
"""
pass
def finditer(self, string, pos=0, endpos=-1):
"""Return an iterator over all non-overlapping matches for the
pattern in string. For each match, the iterator returns a
match object.
:type string: T <= bytes | unicode
:type pos: int
:type endpos: int
:rtype: collections.Iterable[__Match[T]]
"""
pass
def sub(self, repl, string, count=0):
"""Return the string obtained by replacing the leftmost non-overlapping
occurrences of pattern in string by the replacement repl.
:type repl: bytes | unicode | collections.Callable
:type string: T <= bytes | unicode
:type count: int
:rtype: T
"""
pass
def subn(self, repl, string, count=0):
"""Return the tuple (new_string, number_of_subs_made) found by replacing
the leftmost non-overlapping occurrences of pattern with the
replacement repl.
:type repl: bytes | unicode | collections.Callable
:type string: T <= bytes | unicode
:type count: int
:rtype: (T, int)
"""
pass
class __Match(object):
"""Mock class for a match object."""
def __init__(self, pos, endpos, lastindex, lastgroup, re, string):
"""Create a new match object.
:type pos: int
:type endpos: int
:type lastindex: int | None
:type lastgroup: int | bytes | unicode | None
:type re: __Regex
:type string: bytes | unicode
:rtype: __Match[T]
"""
self.pos = pos
self.endpos = endpos
self.lastindex = lastindex
self.lastgroup = lastgroup
self.re = re
self.string = string
def expand(self, template):
"""Return the string obtained by doing backslash substitution on the
template string template.
:type template: T
:rtype: T
"""
pass
def group(self, *args):
"""Return one or more subgroups of the match.
:rtype: T | tuple
"""
pass
def groups(self, default=None):
"""Return a tuple containing all the subgroups of the match, from 1 up
to however many groups are in the pattern.
:rtype: tuple
"""
pass
def groupdict(self, default=None):
"""Return a dictionary containing all the named subgroups of the match,
keyed by the subgroup name.
:rtype: dict[bytes | unicode, T]
"""
pass
def start(self, group=0):
"""Return the index of the start of the substring matched by group.
:type group: int | bytes | unicode
:rtype: int
"""
pass
def end(self, group=0):
"""Return the index of the end of the substring matched by group.
:type group: int | bytes | unicode
:rtype: int
"""
pass
def span(self, group=0):
"""Return a 2-tuple (start, end) for the substring matched by group.
:type group: int | bytes | unicode
:rtype: (int, int)
"""
pass
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -34,21 +35,28 @@ import java.util.List;
* @author vlan
*/
public class PyUserSkeletonsUtil {
public static final String USER_SKELETONS_DIR = "python-skeletons";
@Nullable private static VirtualFile ourUserSkeletonsDirectory;
@NotNull
public static String getUserSkeletonsPath() {
if (ApplicationManager.getApplication().isInternal()) {
return StringUtil.join(new String[] {PathManager.getHomePath(), "python", "helpers", "user-skeletons"}, File.separator);
}
// TODO: Add the possibility to put skeletons into PathManager.getSystemPath() + "/user-skeletons"
return PythonHelpersLocator.getHelperPath("user-skeletons");
private static List<String> getPossibleUserSkeletonsPaths() {
final List<String> result = new ArrayList<String>();
result.add(PathManager.getConfigPath() + File.separator + USER_SKELETONS_DIR);
result.add(ApplicationManager.getApplication().isInternal()
? StringUtil.join(new String[]{PathManager.getHomePath(), "python", "helpers", USER_SKELETONS_DIR}, File.separator)
: PythonHelpersLocator.getHelperPath(USER_SKELETONS_DIR));
return result;
}
@Nullable
public static VirtualFile getUserSkeletonsDirectory() {
if (ourUserSkeletonsDirectory == null) {
ourUserSkeletonsDirectory = LocalFileSystem.getInstance().findFileByPath(getUserSkeletonsPath());
for (String path : getPossibleUserSkeletonsPaths()) {
ourUserSkeletonsDirectory = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
if (ourUserSkeletonsDirectory != null) {
break;
}
}
}
return ourUserSkeletonsDirectory;
}
@@ -75,7 +83,7 @@ public class PyUserSkeletonsUtil {
if (sdk != null) {
final Project project = foothold.getProject();
final PythonSdkPathCache cache = PythonSdkPathCache.getInstance(project, sdk);
final PyQualifiedName cacheQName = PyQualifiedName.fromDottedString("user-skeletons." + qName);
final PyQualifiedName cacheQName = PyQualifiedName.fromDottedString(USER_SKELETONS_DIR + "." + qName);
final List<PsiElement> results = cache.get(cacheQName);
if (results != null) {
final PsiElement element = results.isEmpty() ? null : results.get(0);
@@ -101,7 +109,7 @@ public class PyUserSkeletonsUtil {
}
public static void addUserSkeletonsRoot(@NotNull SdkModificator sdkModificator) {
final VirtualFile root = LocalFileSystem.getInstance().refreshAndFindFileByPath(getUserSkeletonsPath());
final VirtualFile root = getUserSkeletonsDirectory();
if (root != null) {
sdkModificator.addRoot(root, OrderRootType.CLASSES);
}
@@ -126,7 +126,10 @@ public class PythonSdkUpdater implements StartupActivity {
private static void updateSysPath(final Sdk sdk) throws InvalidSdkException {
long start_time = System.currentTimeMillis();
final List<String> sysPath = PythonSdkType.getSysPath(sdk.getHomePath());
sysPath.add(PyUserSkeletonsUtil.getUserSkeletonsPath());
final VirtualFile file = PyUserSkeletonsUtil.getUserSkeletonsDirectory();
if (file != null) {
sysPath.add(file.getPath());
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {