Hlavní navigace

Knihovna Jedi: doplňování kódu a statická analýza kódu v Pythonu (dokončení)

Pavel Tišnovský

Ve druhé části článku o knihovně Jedi si ukážeme další funkce dostupné při statické analýze zdrojových kódů napsaných v Pythonu. Samozřejmě nezapomeneme na ukázku propojení knihovny Jedi s textovým editorem Atom a taktéž s modulem určeným pro slavný Emacs.

Doba čtení: 31 minut

11. Výpis metod začínajících na uvedený prefix

12. Některé chyby, které v knihovně Jedi existují

13. Chování Jedi vs. chování interpretru Pythonu

14. Další pluginy integrující knihovnu Jedi do programátorských textových editorů

15. Použití knihovny Jedi v textovém editoru Atom

16. Ukázky volání funkcí Jedi z Atomu při editaci zdrojových kódů napsaných v Pythonu

17. Použití knihovny Jedi společně s Emacsem

18. Ukázky volání funkcí Jedi z Emacsu při editaci zdrojových kódů napsaných v Pythonu

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Knihovna Jedi: doplňování kódu a statická analýza kódu v Pythonu (dokončení)

V dnešním článku o knihovně Jedi navážeme na obě témata, kterým jsme se věnovali v úvodním článku. Nejprve se seznámíme s dalšími možnostmi, které tato knihovna nabízí při analýze kódu. Bude se jednat například o zjištění funkce, jejíž parametry uživatel právě zapisuje, doplňování jmen metod, nabídky všech metod platných v daném kontextu (v textových editorech se tyto údaje typicky zobrazí po zápisu tečky, popř. tečky následované prefixem jména metody) a taktéž si ukážeme příklad zdrojového kódu, který už knihovna Jedi nedokáže korektně zpracovat. Ve druhé části dnešního článku si ukážeme, jakým způsobem je tato knihovna propojena s programátorskými textovými editory Atom a taktéž Emacs přes vhodný plugin (zajímavé přitom je, že Jedi je využita hned v několika pluginech určených pro Atom i Emacs).

2. Proč dát přednost knihovně Jedi před konkurenčními projekty?

Minule jsme si ve druhé polovině článku popsali způsob použití knihovny Jedi společně s populárním textovým editorem Vim. Dnes si kromě jiného řekneme, jakým způsobem je možné Jedi integrovat do editoru Atom a následně i způsob integrace se slavným operačním systémem textovým editorem Emacs. Taktéž již víme, že existují i další rozhraní mezi knihovnou Jedi a dalšími textovými editory či integrovanými vývojovými prostředími. Například pro Visual Studio Code existuje Python Extension, podobný plugin najdeme pro Eric IDE, Sublime text apod. Nesmíme zapomenout ani na technologii Language Server Protocol, která je v případě programovacího jazyka Python implementována v projektu Python Language Server.

Na tomto místě se čtenář může se zeptat, z jakého důvodu vlastně vzniklo tolik rozhraní a projektů, které používají zrovna knihovnu Jedi a nikoli jiný podobně koncipovaný nástroj. Existují dva hlavní důvody. První z nich je ten, že knihovna Jedi dává při analýze zdrojových souborů psaných v Pythonu poměrně dobré a přesné výsledky (i když ji můžeme v některých případech zmást – viz jeden příklad uvedený níže). A druhým důvodem je fakt, že se jedná o jednoduše nastavitelný a současně i dostatečně „lehkotonážní“ projekt, takže případná tvorba nového pluginu pro nějaký textový editor je relativně jednoduchá (pokud můžu soukromě a navíc trošku neférově porovnat, tak je to mnohem jednodušší, než například provozovat Eclipse v režimu serveru).

Poznámka: druhý důvod bude pravděpodobně stále platit i ve chvíli, kdy se více rozšíří již zmíněný LSP (Language Server Protocol), protože již dnes je knihovna Jedi součástí Python Language Serveru, který LSP používá pro komunikaci s IDE a textovými editory (popř. v případě potřeby i dalšími nástroji).

3. Použití metody follow_definition() pro přečtení podrobnějších informací a navrhovaném identifikátoru

Dnešní první demonstrační příklad bude do určité míry shrnovat informace, které jsme si řekli v úvodním článku. Budeme v něm analyzovat následující (neúplný) skript, v němž je definována třída, funkce, lambda funkce a dvě proměnné – všechny identifikátory přitom začínají na „an“. Na posledním řádku testovacího skriptu je zapsán jen začátek identifikátoru, taktéž „an“:

     1
     2  class anumber:
     3      """Docstring for a class."""
     4      pass
     5
     6  def anagrams(word):
     7      """Very primitive anagram generator."""
     8      if len(word) < 2:
     9          return word
    10      else:
    11          tmp = []
    12          for i, letter in enumerate(word):
    13              for j in anagrams(word[:i]+word[i+1:]):
    14                  tmp.append(j+letter)
    15      return tmp
    16
    17  ann = lambda x,y: x+y
    18  anybody=True
    19  answer="42"
    20  an

Při analýze skriptu umístíme pomyslný kurzor na řádek 20 a sloupec 2. To zajistí následující kód:

lines = src.count('\n')
script = jedi.Script(src, lines+1, len('an'), 'test.py')

Dále můžeme přes knihovnu Jedi zjistit všechny možné identifikátory, které v kontextu řádku 20 začínají na „an“:

completions = script.completions()

Identifikátory, resp. přesněji řečeno jejich jména, si můžeme snadno vypsat:

for completion in completions:
    print(completion.name)

Výsledek by měl vypadat následovně:

anagrams
ann
answer
anumber
any
anybody

Kromě jmen identifikátorů můžeme také získat informace o jejich definici, a to s využitím metody follow_definition() (tyto informace ostatně uvidíme i v pluginem pro Atom a Emacs). Kód pro výpis se nám změní následovně:

for completion in completions:
    print(completion.name)
    print("-"*40)
    definitions = completion.follow_definition()
    print_definitions(definitions)
    print(completion.docstring())
    print("\n"*3)

Pomocná funkce print_definitions() vypíše typ objektu (funkce, třída atd.), jeho plné jméno, jméno modulu, v němž je objekt definovaný a konečně i řádek s definicí:

def print_definitions(definitions):
    if not definitions:
        print("not found")
        return
 
    for definition in definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))

Po spuštění upraveného příkladu dostaneme následující výsledek. Povšimněte si, že jméno modulu „test.py“ odpovídá jménu předanému do konstruktoru Script(); typicky toto jméno automaticky doplňuje textový editor:

anagrams
----------------------------------------
function __main__.anagrams in test.py:6
anagrams(word)
 
Very primitive anagram generator.
 
 
 
 
ann
----------------------------------------
function __main__.<lambda> in test.py:17
 
 
 
 
 
answer
----------------------------------------
instance str in builtins.py:None
 
 
 
 
 
anumber
----------------------------------------
class __main__.anumber in test.py:2
Docstring for a class.
 
 
 
 
any
----------------------------------------
function any in builtins.py:None
Return True if bool(x) is True for any x in the iterable.
 
If the iterable is empty, return False.
 
 
 
 
anybody
----------------------------------------
instance bool in builtins.py:None

Následuje výpis úplného zdrojového kódu dnešního prvního demonstračního příkladu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
class anumber:
    """Docstring for a class."""
    pass
 
def anagrams(word):
    """Very primitive anagram generator."""
    if len(word) < 2:
        return word
    else:
        tmp = []
        for i, letter in enumerate(word):
            for j in anagrams(word[:i]+word[i+1:]):
                tmp.append(j+letter)
    return tmp
 
ann = lambda x,y: x+y
anybody=True
answer="42"
an'''
 
 
def print_definitions(definitions):
    if not definitions:
        print("not found")
        return
 
    for definition in definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
lines = src.count('\n')
script = jedi.Script(src, lines+1, len('an'), 'test.py')
 
completions = script.completions()
 
for completion in completions:
    print(completion.name)
    print("-"*40)
    definitions = completion.follow_definition()
    print_definitions(definitions)
    print(completion.docstring())
    print("\n"*3)

4. Analýza volání funkce

Knihovna Jedi obsahuje i další užitečnou vlastnost – ve chvíli, kdy se kurzor nachází mezi kulatými závorkami za jménem volané funkce, dokáže zjistit přesnější informace o této funkci; dokonce zjistí i pořadí právě zapisovaného parametru. Totéž samozřejmě platí i pro volané metody. To například znamená, že v případě tohoto kódu (kurzor je naznačen šipkou):

print(
      ^

by se vrátila informace o funkci print. Ukažme si tuto užitečnou vlastnost na následujícím skriptu:

     1
     2  def anagrams(word):
     3      """Very primitive anagram generator."""
     4      if len(word) < 2:
     5          return word
     6      else:
     7          tmp = []
     8          for i, letter in enumerate(word):
     9              for j in anagrams(word[:i]+word[i+1:]):
    10                  tmp.append(j+letter)
    11      return tmp
    12
    13  anagrams("pokus")
                  ^

Kurzor umístíme na místo šipky (řádek 13, sloupec 10):

lines = src.count('\n')
script = jedi.Script(src, lines+1, len('anagrams('), 'test.py')

Po konstrukci objektu typu Script získáme seznam všech funkcí, které v daném kontextu přichází do úvahy (vzhledem k dynamice Pythonu nemusí být určení zcela jednoznačné). V našem případě se bude jednat o jedinou funkci, ovšem v obecném případě se může jednat o více záznamů:

call_signatures = script.call_signatures()

Získané signatury si následně v programové smyčce vypíšeme, konkrétně především umístění otevírací závorky a index zadávaného parametru (to opět může pomoci textovým editorům v orientaci ve zdrojovém kódu):

for call_signature in call_signatures:
    print(call_signatures.__str__(), call_signature.index, call_signature.bracket_start)

Výsledek vypsaný předchozí programovou smyčkou by měl vypadat následovně:

[<CallSignature: anagrams index 0>] 0 (13, 8)

Tento výsledek je možné přečíst následovně: uživatel právě doplňuje argumenty pro funkci anagrams, přičemž se jedná o první argument (s indexem 0) a otevírací závorka byla nalezena na pozici [13, 8], tj. na třináctém řádku a sloupci 8.

Samozřejmě si opět ukážeme celý zdrojový kód tohoto příkladu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
def anagrams(word):
    """Very primitive anagram generator."""
    if len(word) < 2:
        return word
    else:
        tmp = []
        for i, letter in enumerate(word):
            for j in anagrams(word[:i]+word[i+1:]):
                tmp.append(j+letter)
    return tmp
 
anagrams("pokus")'''
 
lines = src.count('\n')
script = jedi.Script(src, lines+1, len('anagrams('), 'test.py')
 
call_signatures = script.call_signatures()
 
for call_signature in call_signatures:
    print(call_signatures.__str__(), call_signature.index, call_signature.bracket_start)

5. Složitější příklad pro analýzu volaných funkcí

Pro úplnost se podívejme na poněkud složitější příklad. Testovací skript od řádku 11 obsahuje hned několik volaných funkcí, které mají různý počet parametrů, otevírací levá kulatá závorka nemusí následovat hned na jménem funkce a v posledním případu je dokonce otevírací závorka na zcela jiném řádku, než první argument (i tento styl zápisu Python umožňuje, i když není zcela přesně v souladu s PEP-8). Šipkami ^ jsou naznačena umístění kurzoru, pro něž budeme žádat analýzu:

     1
     2  def foo():
     3      return 0
     4
     5  def bar(x):
     6      return 1
     7
     8  def baz(x,y):
     9      return 2
    10
    11  foo()
           ^
    12  bar(1)
            ^
    13  baz(1,2)
              ^
    14  baz(foo(), bar(1))
                       ^
    15  print(10, 20, 30, 40, 50)
                             ^
    16  print   (10, 20, 30, 40, 50)
                                ^
    17  print(
    18
    19  42)
        ^

Pro všech sedm umístění kurzoru by se měly vrátit následující údaje. Povšimněte si, že místo zápisu levé otevírací závorky je vždy vypočteno správně, a to i v poslední funkci (opět velká pomoc pro tvůrce pluginů) a u funkce foo(), která nemá žádné parametry, se namísto indexu zapisovaného argumentu objevila hodnota None:

[<CallSignature: foo index None>] None (11, 3)
 
[<CallSignature: bar index 0>] 0 (12, 3)
 
[<CallSignature: baz index 1>] 1 (13, 3)
 
[<CallSignature: bar index 0>] 0 (14, 14)
 
[<CallSignature: print index 4>] 4 (15, 5)
 
[<CallSignature: print index 4>] 4 (16, 8)
 
[<CallSignature: print index 0>] 0 (17, 5)

Jak již zajisté, očekáváte, opět si ukážeme celý zdrojový kód tohoto příkladu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
def foo():
    return 0
 
def bar(x):
    return 1
 
def baz(x,y):
    return 2
 
foo()
bar(1)
baz(1,2)
baz(foo(), bar(1))
print(10, 20, 30, 40, 50)
print   (10, 20, 30, 40, 50)
print(
 
42)
'''
 
 
def print_call_signatures(script):
    call_signatures = script.call_signatures()
 
    for call_signature in call_signatures:
        print(call_signatures.__str__(), call_signature.index, call_signature.bracket_start)

    print()
 
 
lines = src.count('\n')
 
script = jedi.Script(src, 11, len('foo('), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, 12, len('bar(1'), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, 13, len('baz(1,2'), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, 14, len('baz(foo(), bar(1'), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, 15, len('print(10, 20, 30, 40, '), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, 16, len('print   (10, 20, 30, 40, '), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, lines, 1, 'test.py')
print_call_signatures(script)

6. Volání metod namísto obyčejných funkcí

V dalším demonstračním příkladu, jehož zdrojový kód naleznete na této adrese je ukázáno, že metoda Script.call_signatures() je použitelná nejenom pro zjištění dalších informací o volaných funkcích, ale i v případě, že jsou volány metody. Skript, na němž je tento příklad vyzkoušen, je velmi jednoduchý (ve skriptu jsou opět naznačeny pozice kurzorů):

     1
     2  class C1:
     3      def foo(self):
     4          return 0
     5
     6      def bar(self, x):
     7          return 1
     8
     9      def baz(self, x,y):
    10          return 2
    11
    12  obj = C1()
    13  obj.foo()
               ^
    14  obj.bar(1)
                ^
    15  obj.baz(1,2)
                  ^
    16  obj.baz(obj.foo(), obj.bar(1))
                                   ^

Výsledek analýzy volaných metod vypadá takto (v tomto případě se nijak zvlášť nerozlišuje mezi voláním funkce a voláním metody):

[<CallSignature: foo index None>] None (13, 7)
 
[<CallSignature: bar index 0>] 0 (14, 7)
 
[<CallSignature: baz index 1>] 1 (15, 7)
 
[<CallSignature: bar index 0>] 0 (16, 26)

A nakonec si opět ukažme zdrojový kód celého příkladu, který volá funkce a metody knihovny Jedi:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
class C1:
    def foo(self):
        return 0
 
    def bar(self, x):
        return 1
 
    def baz(self, x,y):
        return 2
 
obj = C1()
obj.foo()
obj.bar(1)
obj.baz(1,2)
obj.baz(obj.foo(), obj.bar(1))'''
 
 
def print_call_signatures(script):
    call_signatures = script.call_signatures()
 
    for call_signature in call_signatures:
        print(call_signatures.__str__(), call_signature.index, call_signature.bracket_start)
 
    print()
 
 
lines = src.count('\n')
 
script = jedi.Script(src, lines-2, len('obj.foo('), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, lines-1, len('obj.bar(1'), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, lines, len('obj.baz(1,2'), 'test.py')
print_call_signatures(script)
 
script = jedi.Script(src, lines+1, len('obj.baz(obj.foo(), obj.bar(1'), 'test.py')
print_call_signatures(script)

7. Malé zopakování z minula – dynamické chování Pythonu a jeho vliv na funkci knihovny Jedi

V této kapitole si jen pro připomenutí ukážeme, jakým způsobem se projeví dynamické chování interpretru Pythonu na analýze kódu. V dalším skriptu máme (kromě dalších věcí) deklarovanou i funkci answer(), ovšem v průběhu statické analýzy kódu není možné zjistit, která ze tří možných deklarací bude skutečně použita. Jedná se samozřejmě o poněkud umělý příklad, ale někdy se s podobně koncipovaným kódem můžete setkat:

     1
     2  class anumber:
     3      """Docstring for a class."""
     4      pass
     5
     6
     7  if random.random() < 0.5:
     8      def answer():
     9          """1st variant of answer function."""
    10          return "42"
    11  elif random.random() < 0.5:
    12      def answer():
    13          """2nd variant of answer function."""
    14          return 42
    15  else:
    16      def answer():
    17          """3rd variant of answer function."""
    18          return [42]
    19
    20
    21  def anagrams(word):
    22      """Very primitive anagram generator."""
    23      if len(word) < 2:
    24          return word
    25      else:
    26          tmp = []
    27          for i, letter in enumerate(word):
    28              for j in anagrams(word[:i]+word[i+1:]):
    29                  tmp.append(j+letter)
    30      return tmp
    31
    32  ann = lambda x,y: x+y
    33  anybody=True
    34  print(answer())

Při zavolání metody Script.goto_definitions() na posledním (patnáctém) řádku nemůže knihovna Jedi rozhodnout, o kterou funkci se jedná a z tohoto důvodu vrátí všechny tři možné varianty, což je pravděpodobně v tomto případě nejkorektnější chování:

function __main__.answer in test.py:8
function __main__.answer in test.py:12
function __main__.answer in test.py:16

Celý příklad, který zjistí informaci o volané funkci answer(), vypadá následovně:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
class anumber:
    """Docstring for a class."""
    pass
 
 
if random.random() < 0.5:
    def answer():
        """1st variant of answer function."""
        return "42"
elif random.random() < 0.5:
    def answer():
        """2nd variant of answer function."""
        return 42
else:
    def answer():
        """3rd variant of answer function."""
        return [42]
 
 
def anagrams(word):
    """Very primitive anagram generator."""
    if len(word) < 2:
        return word
    else:
        tmp = []
        for i, letter in enumerate(word):
            for j in anagrams(word[:i]+word[i+1:]):
                tmp.append(j+letter)
    return tmp
 
ann = lambda x,y: x+y
anybody=True
print(answer())'''
 
 
def print_definitions(definitions):
    if not definitions:
        print("not found")
        return
 
    for definition in definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
lines = src.count('\n')
script = jedi.Script(src, lines+1, len('print('), 'test.py')
 
definitions = script.goto_definitions()
 
print_definitions(definitions)

8. Podrobnější informace o jménech funkcí v případě, že kód nelze staticky jednoznačně analyzovat

Zajímavé je, že při návrhu jména funkce/metody/proměnné, které se má na nějakém místě doplnit, pracuje knihovna Jedi poněkud odlišně. Následující testovací skript je prakticky kompletně zkopírován ze skriptu předchozího, jen s tím rozdílem, že na řádku číslo 34 budeme chtít vypsat všechny identifikátory, které se mohou na tomto místě doplnit:

     1
     2  class anumber:
     3      """Docstring for a class."""
     4      pass
     5
     6
     7  if random.random() < 0.5:
     8      def answer():
     9          """1st variant of answer function."""
    10          return "42"
    11  elif random.random() < 0.5:
    12      def answer():
    13          """2nd variant of answer function."""
    14          return 42
    15  else:
    16      def answer():
    17          """3rd variant of answer function."""
    18          return [42]
    19
    20
    21  def anagrams(word):
    22      """Very primitive anagram generator."""
    23      if len(word) < 2:
    24          return word
    25      else:
    26          tmp = []
    27          for i, letter in enumerate(word):
    28              for j in anagrams(word[:i]+word[i+1:]):
    29                  tmp.append(j+letter)
    30      return tmp
    31
    32  ann = lambda x,y: x+y
    33  anybody=True
    34  an
          ^

Podívejme se, jaké možnosti získáme – nyní se funkce answer nabízí pouze jedenkrát, a to ve své poslední (třetí) variantě, což není příliš intuitivní:

anagrams
----------------------------------------
function __main__.anagrams in test.py:21
anagrams(word)
 
Very primitive anagram generator.
 
 
 
 
ann
----------------------------------------
function __main__. in test.py:32
 
 
 
 
 
answer
----------------------------------------
function __main__.answer in test.py:16
answer()
 
3rd variant of answer function.
 
 
 
 
anumber
----------------------------------------
class __main__.anumber in test.py:2
Docstring for a class.
 
 
 
 
any
----------------------------------------
function any in builtins.py:None
Return True if bool(x) is True for any x in the iterable.
 
If the iterable is empty, return False.
 
 
 
 
anybody
----------------------------------------
instance bool in builtins.py:None

Předchozí výstup byl vytvořen tímto příkladem, který nejprve získá informace o všech možnostech pro doplnění symbolu a posléze zobrazí i definici příslušného symbolu a jeho dokumentační řetězec:

completions = script.completions()
 
for completion in completions:
    print(completion.name)
    definitions = completion.follow_definition()
    print(completion.docstring())
    print("\n"*3)

Úplný zdrojový kód příkladu vypadá takto:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
class anumber:
    """Docstring for a class."""
    pass
 
 
if random.random() < 0.5:
    def answer():
        """1st variant of answer function."""
        return "42"
elif random.random() < 0.5:
    def answer():
        """2nd variant of answer function."""
        return 42
else:
    def answer():
        """3rd variant of answer function."""
        return [42]
 
 
def anagrams(word):
    """Very primitive anagram generator."""
    if len(word) < 2:
        return word
    else:
        tmp = []
        for i, letter in enumerate(word):
            for j in anagrams(word[:i]+word[i+1:]):
                tmp.append(j+letter)
    return tmp
 
ann = lambda x,y: x+y
anybody=True
an'''
 
 
def print_definitions(definitions):
    if not definitions:
        print("not found")
        return
 
    for definition in definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
lines = src.count('\n')
script = jedi.Script(src, lines+1, len('an'), 'test.py')
 
completions = script.completions()
 
for completion in completions:
    print(completion.name)
    print("-"*40)
    definitions = completion.follow_definition()
    print_definitions(definitions)
    print(completion.docstring())
    print("\n"*3)

9. Doplnění jmen volaných metod

V prakticky všech vyspělejších integrovaných vývojových prostředích se uživatel setká i s automatickým doplněním jména volané metody popř. s nabídkou seznamu metod, které je možné v daném místě kódu doplnit. Knihovna Jedi tuto funkci samozřejmě podporuje, takže si v navazujících dvou kapitolách ukážeme způsob získání všech metod nějakého objektu (typicky po zápisu tečky v programátorském editoru či v IDE) a taktéž těch metod, jejichž jméno začíná na nějaký prefix. Obě funkce jsou vlastně implementovány naprosto stejným kódem – jediným rozdílem je fakt, že po tečce lze zapsat jakoukoli metodu, zatímco zapsáním prefixu se seznam možných metod (které dávají v daném kontextu význam) zmenšuje.

Obrázek 1: Nabídka dostupných metod ve vývojovém prostředí Eclipse (zdrojový kód je psán v Javě).

10. Výpis všech metod objektu

Nejprve si ukažme způsob výpisu všech metod objektu. V následujícím testovacím skriptu si povšimněte, že kurzor bude umístěn na tečku oddělující jméno objektu od (nějaké) metody, takže na tomto místě je skutečně možné zavolat libovolnou metodu pro daný objekt:

class C1:
    def foo(self):
        """Function foo defined in class C1."""
        return 1
 
class C2:
    def foo(self):
        """Function foo defined in class C2."""
        return 2
 
class C3:
    def foo(self):
        """Function foo defined in class C3."""
        return 2
 
obj = C2()
obj.'''
   ^

Výsledkem bude sáhodlouhá zpráva o všech dostupných metodách. Pro větší přehlednost jsou názvy metod zvýrazněny:

foo
----------------------------------------
function __main__.foo in test.py:8
foo(self)
 
Function foo defined in class C2.
 
 
 
 
__class__
----------------------------------------
class object.type in builtins.py:None
type(object_or_name, bases, dict)
type(object) -> the object's type
type(name, bases, dict) -> a new type
 
 
 
 
__delattr__
----------------------------------------
function object.__delattr__ in builtins.py:None
Implement delattr(self, name).
 
 
 
 
__dir__
----------------------------------------
function object.__dir__ in builtins.py:None
__dir__() -> list
default dir() implementation
 
 
 
 
__doc__
----------------------------------------
instance object.str in builtins.py:None
str(object='') -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str
 
Create a new string object from the given object. If encoding or
errors is specified, then the object must expose a data buffer
that will be decoded using the given encoding and error handler.
Otherwise, returns the result of object.__str__() (if defined)
or repr(object).
encoding defaults to sys.getdefaultencoding().
errors defaults to 'strict'.
 
 
 
 
__eq__
----------------------------------------
function object.__eq__ in builtins.py:None
Return self==value.
 
 
 
 
__format__
----------------------------------------
function object.__format__ in builtins.py:None
default object formatter
 
 
 
 
__ge__
----------------------------------------
function object.__ge__ in builtins.py:None
Return self>=value.
 
 
 
 
__getattribute__
----------------------------------------
function object.__getattribute__ in builtins.py:None
Return getattr(self, name).
 
 
 
 
__gt__
----------------------------------------
function object.__gt__ in builtins.py:None
Return self>value.
 
 
 
 
__hash__
----------------------------------------
function object.__hash__ in builtins.py:None
Return hash(self).
 
 
 
 
__init__
----------------------------------------
function object.__init__ in builtins.py:None
Initialize self.  See help(type(self)) for accurate signature.
 
 
 
 
__init_subclass__
----------------------------------------
function object.__init_subclass__ in builtins.py:None
This method is called when a class is subclassed.
 
The default implementation does nothing. It may be
overridden to extend subclasses.
 
 
 
 
__le__
----------------------------------------
function object.__le__ in builtins.py:None
Return self<=value.
 
 
 
 
__lt__
----------------------------------------
function object.__lt__ in builtins.py:None
Return self<value.
 
 
 
 
__ne__
----------------------------------------
function object.__ne__ in builtins.py:None
Return self!=value.
 
 
 
 
__new__
----------------------------------------
function object.__new__ in builtins.py:None
Create and return a new object.  See help(type) for accurate signature.
 
 
 
 
__reduce__
----------------------------------------
function object.__reduce__ in builtins.py:None
helper for pickle
 
 
 
 
__reduce_ex__
----------------------------------------
function object.__reduce_ex__ in builtins.py:None
helper for pickle
 
 
 
 
__repr__
----------------------------------------
function object.__repr__ in builtins.py:None
Return repr(self).
 
 
 
 
__setattr__
----------------------------------------
function object.__setattr__ in builtins.py:None
Implement setattr(self, name, value).
 
 
 
 
__sizeof__
----------------------------------------
function object.__sizeof__ in builtins.py:None
__sizeof__() -> int
size of object in memory, in bytes
 
 
 
 
__str__
----------------------------------------
function object.__str__ in builtins.py:None
Return str(self).
 
 
 
 
__subclasshook__
----------------------------------------
function object.__subclasshook__ in builtins.py:None
Abstract classes can override this to customize issubclass().
 
This is invoked early on by abc.ABCMeta.__subclasscheck__().
It should return True, False or NotImplemented.  If it returns
NotImplemented, the normal algorithm is used.  Otherwise, it
overrides the normal algorithm (and the outcome is cached).
 
 
 
 

Tento výsledek lze snadno získat programem:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
class C1:
    def foo(self):
        """Function foo defined in class C1."""
        return 1
 
class C2:
    def foo(self):
        """Function foo defined in class C2."""
        return 2
 
class C3:
    def foo(self):
        """Function foo defined in class C3."""
        return 2
 
obj = C2()
obj.'''
 
 
def print_definitions(definitions):
    if not definitions:
        print("not found")
        return
 
    for definition in definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
lines = src.count('\n')
script = jedi.Script(src, lines+1, len('obj.'), 'test.py')
 
completions = script.completions()
 
for completion in completions:
    print(completion.name)
    print("-"*40)
    definitions = completion.follow_definition()
    print_definitions(definitions)
    print(completion.docstring())
    print("\n"*3)

11. Výpis metod začínajících na uvedený prefix

Jen nepatrnou úpravou předchozího programu získáme seznam všech metod, které začínají na zadaný prefix. Změní se v podstatě jen dvě věci. Samotný testovaný skript:

class C1:
    def foo(self):
        """Function foo defined in class C1."""
        return 1
 
class C2:
    def foo(self):
        """Function foo defined in class C2."""
        return 2
 
class C3:
    def foo(self):
        """Function foo defined in class C3."""
        return 2
 
obj = C2()
obj.fo'''
    ^

A v následujícím řádku posuneme pomyslný kurzor o dva znaky doprava:

script = jedi.Script(src, lines+1, len('obj.fo'), 'test.py')

Výsledkem bude seznam obsahující jen jedinou metodu, a to konkrétně naši metodu C2.foo():

foo
----------------------------------------
function __main__.foo in test.py:8
foo(self)
 
Function foo defined in class C2.

Pro úplnost si ukažme celý příklad:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
class C1:
    def foo(self):
        """Function foo defined in class C1."""
        return 1
 
class C2:
    def foo(self):
        """Function foo defined in class C2."""
        return 2
 
class C3:
    def foo(self):
        """Function foo defined in class C3."""
        return 2
 
obj = C2()
obj.fo'''
 
 
def print_definitions(definitions):
    if not definitions:
        print("not found")
        return
 
    for definition in definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
lines = src.count('\n')
script = jedi.Script(src, lines+1, len('obj.fo'), 'test.py')
 
completions = script.completions()
 
for completion in completions:
    print(completion.name)
    print("-"*40)
    definitions = completion.follow_definition()
    print_definitions(definitions)
    print(completion.docstring())
    print("\n"*3)

12. Některé chyby, které v knihovně Jedi existují

Knihovna Jedi pochopitelně není zcela bezchybná, takže se můžeme – i když ne příliš často – setkat s nápovědou nebo analýzou kódu, která nebude odpovídat běžnému chování interpretru Pythonu. Jedna z těchto chyb je naznačena v dalším příkladu. Nejprve se podívejme na testovací skript. Jsou v něm deklarovány tři třídy C1, C2 a C3, přičemž v každé z těchto tříd je vytvořena metoda nazvaná foo(). Tuto část Jedi samozřejmě bez problémů zpracuje. Ovšem ve druhé polovině skriptu máme programovou konstrukci if-elseif-else, v níž se přiřazuje hodnota k nově vytvářené proměnné obj. Na samotném konci skriptu pak budeme chtít doplnit jméno metody a současně si i zobrazit to místo v programovém kódu, v němž je metoda deklarována:

     1
     2  class C1:
     3      def foo(self):
     4          """Function foo defined in class C1."""
     5          return 1
     6
     7  class C2:
     8      def foo(self):
     9          """Function foo defined in class C2."""
    10          return 2
    11
    12  class C3:
    13      def foo(self):
    14          """Function foo defined in class C3."""
    15          return 2
    16
    17  if True:
    18      obj = C1()
    19  elif True:
    20      obj = C2()
    21  else:
    22      obj = C3()
    23  obj.fo
             ^

13. Chování Jedi vs. chování interpretru Pythonu

Pokud se skript ukázaný v předchozí kapitole pokusíme analyzovat knihovnou Jedi, dostaneme informaci o tom, že se na řádku 23 má doplnit jméno metody foo (což je zcela správně), ovšem následuje další informace o tom, že tato metoda je deklarována ve třídě C2:

foo
----------------------------------------
function __main__.foo in test.py:8
foo(self)
 
Function foo defined in class C2.

Můžeme si samozřejmě ukázat chování interpretru Pythonu, které plně odpovídá očekávání – ve skutečnosti se do obj přiřadí instance třídy C1 a tudíž se o několik řádků níže zavolá metoda C1.foo():

class C1:
    def foo(self):
        """Function foo defined in class C1."""
        return 1
 
class C2:
    def foo(self):
        """Function foo defined in class C2."""
        return 2
 
class C3:
    def foo(self):
        """Function foo defined in class C3."""
        return 2
 
if True:
    obj = C1()
elif True:
    obj = C2()
else:
    obj = C3()
 
print(obj)
print(obj.foo())

Po spuštění dostaneme korektní informaci o třídě C1 a její metodě C1.foo():

<__main__.C1 object at 0x7fe665617278>
1

Analýza ukázaná na začátku této kapitoly byla vytvořena programem jedi19_howto_confuse_jedi.py:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
class C1:
    def foo(self):
        """Function foo defined in class C1."""
        return 1
 
class C2:
    def foo(self):
        """Function foo defined in class C2."""
        return 2
 
class C3:
    def foo(self):
        """Function foo defined in class C3."""
        return 2
 
if True:
    obj = C1()
elif True:
    obj = C2()
else:
    obj = C3()
obj.fo'''
 
 
def print_definitions(definitions):
    if not definitions:
        print("not found")
        return
 
    for definition in definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
lines = src.count('\n')
script = jedi.Script(src, lines+1, len('obj.fo'), 'test.py')
 
completions = script.completions()
 
for completion in completions:
    print(completion.name)
    print("-"*40)
    definitions = completion.follow_definition()
    print_definitions(definitions)
    print(completion.docstring())
    print("\n"*3)

14. Další pluginy integrující knihovnu Jedi do programátorských textových editorů

Ve druhé části článku si ukážeme, jakým způsobem je vyřešena integrace knihovny Jedi s dalšími programátorskými textovými editory. Minule jsme si ukázali integraci s Vimem, a to konkrétně s využitím pluginu nazvaného jedi-vim. Dnes se zaměříme na textový editor Atom a samozřejmě také na Emacs (důvody, proč je Jedi použit v mnoha pluginech, byly napsány ve druhé kapitole).

Obrázek 2: Nápověda k minule popsanému pluginu jedi-vim.

Poznámka: v případě Atomu se budeme zabývat skutečně „pouze“ samotným textovým editorem a jeho pluginy; nebude se tedy jednat o popis Atom IDE, protože popis možností Atom IDE by si vyžádal samostatný článek.

Obrázek 3: Zobrazení dokumentačního řetězce pro identifikátor, nad nímž se nachází kurzor. Tato funkce je dostupná po stisku klávesové zkratky K.

15. Použití knihovny Jedi v textovém editoru Atom

Před popisem možností integrace knihovny Jedi s Atomem si pro úplnost řekněme, jak se Atom nainstaluje. V případě, že balíčky s Atomem nemáte přímo v repositáři vaší distribuce (popř. když tyto balíčky zastaraly), můžete si přímo ze stránek https://atom.io/ příslušný balíček stáhnout a následně si ho i nainstalovat:

Stažení balíčku pro distribuce založené na RPM:

$ wget -o atom.rpm https://atom.io/download/rpm

Stažení balíčku pro distribuce založené na Debianních balíčcích:

$ wget -o atom.deb https://atom.io/download/deb

Ve chvíli, kdy máme stažený soubor atom.rpm (má velikost přibližně 132 MB!), můžeme přistoupit k vlastní instalaci. Ta je pro distribuce založené na RPM velmi jednoduchá:

# dnf install atom.rpm
 
Last metadata expiration check: 1:09:13 ago on Tue 21 Aug 2018 10:00:34 AM EDT.
Dependencies resolved.
================================================================================
 Package          Arch      Version           Repository                   Size
================================================================================
Installing:
 atom             x86_64    1.29.0-0.1        @commandline                131 M
Installing dependencies:
 libXScrnSaver    x86_64    1.2.2-14.fc28     Fedora-Everything            29 k
 
Transaction Summary
================================================================================
Install  2 Packages
 
Total size: 131 M
Total download size: 29 k
Installed size: 523 M
Is this ok [y/N]:

Další průběh instalace:

Downloading Packages:
libXScrnSaver-1.2.2-14.fc28.x86_64.rpm          1.1 MB/s |  29 kB     00:00
--------------------------------------------------------------------------------
Total                                           1.0 MB/s |  29 kB     00:00
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                        1/1
  Installing       : libXScrnSaver-1.2.2-14.fc28.x86_64                     1/2
  Running scriptlet: libXScrnSaver-1.2.2-14.fc28.x86_64                     1/2
  Installing       : atom-1.29.0-0.1.x86_64                                 2/2
  Running scriptlet: atom-1.29.0-0.1.x86_64                                 2/2
  Verifying        : atom-1.29.0-0.1.x86_64                                 1/2
  Verifying        : libXScrnSaver-1.2.2-14.fc28.x86_64                     2/2
 
Installed:
  atom.x86_64 1.29.0-0.1           libXScrnSaver.x86_64 1.2.2-14.fc28
 
Complete!

Po instalaci si již můžete (jako běžný uživatel, ne Root), textový editor Atom spustit jednoduše:

$ atom

Dále je nutné nainstalovat plugin nazvaný autocomplete-python-jedi:

Obrázek 4: V okně „Install Packages“ vyhledáme všechny (nové) pluginy, v jejichž jméně se nachází řetězec „Jedi“.

Obrázek 5: Vybereme balíček pojmenovaný „autocomplete-python-jedi“.

Obrázek 6: Po nainstalování se přesvědčíme, že balíček není zakázán (disabled). Žádné možnosti nastavení vlastně ani nejsou k dispozici.

16. Ukázky volání funkcí Jedi z Atomu při editaci zdrojových kódů napsaných v Pythonu

Po úspěšné instalaci pluginu „autocomplete-python-jedi“ si můžeme vyzkoušet, jaké nové funkce jsou v Atomu dostupné. Opět se podívejme na screenshoty s krátkým popisem:

Obrázek 7: Nápověda (přesněji řečeno dokumentační řetězec) k právě zadávané funkci nebo metodě se zobrazí automaticky (po nepatrné časové prodlevě).

Obrázek 8: Zobrazení seznamu funkcí/metod/proměnných obsahujících buď přímo zadaný prefix, nebo (což je potenciálně velmi užitečné) sekvenci zadaných znaků kdekoli v názvu. Na prvním místě jsou samozřejmě zobrazeny ty identifikátory, které začínají prefixem. Výchozí klávesová zkratka je Ctrl+Space. Podobným způsobem se zobrazí metody objektu apod.

Obrázek 9: Kurzor se nachází na volání funkce.

Obrázek 10: A po stisku poněkud kostrbaté klávesové zkratky Ctrl+Alt+G se provedl skok na její definici.

Obrázek 11: Zobrazení všech výskytů nějaké objektu (funkce, metody, proměnné).

Obrázek 12: Pokud si navíc nainstalujete plugin nazvaný „Hyperclick“, bude možné namísto Ctrl+Alt+G použít zkratku Ctrl+klik myší (což ovšem koliduje s přidáním multikurzoru, takže se tato zkratka musela přesunout).

Obrázek 13: Použití kombinace pluginů „Hyperclick“ a „autocomplete-python-jedi“.

17. Použití knihovny Jedi společně s Emacsem

V článcích, v nichž se zmiňujeme o programátorských textových editorech, samozřejmě není možné nezmínit slavný Emacs, který za sebou má dosti dlouhou historii, v jejímž průběhu vzniklo jeho několik forků. Pro Emacs existuje hned několik pluginů volajících knihovnu Jedi; my si dnes popíšeme plugin pojmenovaný jednoduše jedi. Existuje několik způsobů instalace tohoto pluginu. Nejjednodušší je instalace ve chvíli, kdy máte v Emacsu povolen repositář balíčků MELPA (ten obsahuje přibližně 3900 balíčků!). Po zadání příkazu:

Alt-X list-packages

je možné zjistit, jestli je MELPA povolena a zda je tedy balíček jedi přímo k dispozici:

Obrázek 14: V případě, že MELPA není povolena, budou dostupné jen balíčky z repositáře ELPA (Emacs Lisp Package Archive).

Pokud tomu tak není, je nutné MELPu povolit, například těmito řádky přidanými do souboru .emacs:

(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)

Po opětovném načtení editoru zkuste znovu zadat příkaz:

Alt-X list-packages

Tentokrát by se měl objevit mj. i balíček nazvaný jedi (pozor na to, že prvotní načtení všech 3900 balíčků může chvíli trvat):

Obrázek 15: V případě, že MELPA je povolena, měl by se v seznamu balíčků objevit i plugin jedi.

Plugin jedi nainstalujeme běžným způsobem:

Obrázek 16: Instalace pluginu jedi.

Následně přidáme další dva řádky do souboru .emacs:

(add-hook 'python-mode-hook 'jedi:setup)
(setq jedi:complete-on-dot t)

Takže jeho výsledný obsah může vypadat přibližně takto:

(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
 
;; Added by Package.el.  This must come before configurations of
;; installed packages.  Don't delete this line.  If you don't want it,
;; just comment it out by adding a semicolon to the start of the line.
;; You may delete these explanatory comments.
(package-initialize)
 
(add-hook 'python-mode-hook 'jedi:setup)
(setq jedi:complete-on-dot t)
 
(custom-set-variables
 ;; custom-set-variables was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(inhibit-startup-screen t)
 '(package-selected-packages (quote (jedi diffview ##))))
 
(custom-set-faces
 ;; custom-set-faces was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 )

18. Ukázky volání funkcí Jedi z Emacsu při editaci zdrojových kódů napsaných v Pythonu

Po načtení souboru s koncovkou „.py“ se Emacs přepne do režimu Pythonu a současně by se na pozadí měla spustit knihovna Jedi ve vlastním virtuálním prostředí (virtualenv). Ta umožní například následující operace připomínající skutečné IDE:

Obrázek 17: Zobrazení seznamu objektů (funkcí, metod atd.) začínajících daným prefixem. U vybraného objektu se zobrazí jeho nápověda (dokumentační řetězec).

Obrázek 18: Zúžení výběru po zadání delšího prefixu.

Obrázek 19: Zobrazení všech metod objektu.

Obrázek 20: Zde si povšimněte, že se zobrazí námi zapsaný dokumentační řetězec (Jedi má přístup k editovanému souboru).

Obrázek 21: Zápis prefixu jména třídy, opět se zobrazením dokumentačního řetězce.

Poznámka: některé další možnosti kombinace Emacsu a Jedi budou popsány v miniseriálu o Emacsu ve funkci IDE.

19. Repositář s demonstračními příklady

Všechny dnes popisované demonstrační příklady ukazující některé možnosti knihovny Jedi byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/pre­sentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:

# Příklad Popis Odkaz
1 jedi11_follow_definitions.py použití metody follow_definition() https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi11_follow_definiti­ons.py
2 jedi12_call_signatures.py použití metody Script.call_signatures() https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi12_call_signatures­.py
3 jedi13_more_difficult_call_sig­natures.py použití metody Script.call_signatures() https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi13_more_difficult_ca­ll_signatures.py
4 jedi14_method_signatures.py signatury volaných metod (ne funkcí) https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi14_method_signatures­.py
5 jedi15_goto_definitions_ran­dom_function_declaration.py dynamické chování Pythonu ovlivňující chování Jedi https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi15_goto_definition­s_random_function_declara­tion.py
6 jedi16_completion_follow_definition.py dtto jako předchozí příklad, ovšem pro follow_definition() https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi16_completion_follow_de­finition.py
7 jedi17_method_completion_an­d_definition.py korektní doplnění jména metody https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi17_method_completi­on_and_definition.py
8 jedi18_all_methods_comple­tion_and_definition.py korektní doplnění jména metody https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi18_all_methods_com­pletion_and_definition.py
9 jedi19_howto_confuse_jedi.py ukázka jednoduchého zmatení Jedi https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi19_howto_confuse_je­di.py

20. Odkazy na Internetu

  1. Jedi – an awesome autocompletion/static analysis library for Python
    https://jedi.readthedocs.i­o/en/latest/index.html
  2. Jedi API Overview
    https://jedi.readthedocs.i­o/en/latest/docs/api.html
  3. jedi-vim
    https://github.com/davidhalter/jedi-vim
  4. YouCompleteMe: A code-completion engine for Vim
    https://valloric.github.i­o/YouCompleteMe/
  5. Elpy: Emacs Python Development Environment
    https://github.com/jorgen­schaefer/elpy
  6. Emacs-Jedi
    https://github.com/tkf/emacs-jedi
  7. Python Autocomplete Jedi Package
    https://atom.io/packages/autocomplete-python-jedi
  8. Autocomplete (Wikipedia)
    https://en.wikipedia.org/wi­ki/Autocomplete
  9. Seriál Textový editor Vim jako IDE (zde na Root.cz)
    https://www.root.cz/serialy/textovy-editor-vim-jako-ide/
  10. Jedi: A completion library for Python
    https://www.masteringemac­s.org/article/jedi-completion-library-python
  11. Jedi.el – Python auto-completion for Emacs (GitHub)
    https://github.com/tkf/emacs-jedi
  12. Jedi.el – Python auto-completion for Emacs (dokumentace, popis možností apod.)
    http://tkf.github.io/emacs-jedi/latest/
  13. Vim – the ubiquitous text editor
    https://www.vim.org/
  14. GNU Emacs
    https://www.gnu.org/software/emacs/
  15. EmacsWiki
    https://www.emacswiki.org/e­macs/EmacsWiki
  16. Atom (stránka textového editoru)
    https://atom.io/
  17. el-get (GitHub)
    https://github.com/dimitri/el-get
  18. MELPA
    https://www.emacswiki.org/emacs/MELPA
  19. Atom IDE
    https://ide.atom.io/
  20. Sublime Text as Python IDE – jedi
    https://screamingatmyscre­en.com/2013/9/sublime-text-as-python-ide-jedi/
Našli jste v článku chybu?