Obsah
1. Knihovna Jedi: doplňování kódu a statická analýza kódu v Pythonu (dokončení)
2. Proč dát přednost knihovně Jedi před konkurenčními projekty?
5. Složitější příklad pro analýzu volaných funkcí
6. Volání metod namísto obyčejných funkcí
7. Malé zopakování z minula – dynamické chování Pythonu a jeho vliv na funkci knihovny Jedi
8. Podrobnější informace o jménech funkcí v případě, že kód nelze staticky jednoznačně analyzovat
9. Doplnění jmen volaných metod
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
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).
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.
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.
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/presentations. 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ář:
20. Odkazy na Internetu
- Jedi – an awesome autocompletion/static analysis library for Python
https://jedi.readthedocs.io/en/latest/index.html - Jedi API Overview
https://jedi.readthedocs.io/en/latest/docs/api.html - jedi-vim
https://github.com/davidhalter/jedi-vim - YouCompleteMe: A code-completion engine for Vim
https://valloric.github.io/YouCompleteMe/ - Elpy: Emacs Python Development Environment
https://github.com/jorgenschaefer/elpy - Emacs-Jedi
https://github.com/tkf/emacs-jedi - Python Autocomplete Jedi Package
https://atom.io/packages/autocomplete-python-jedi - Autocomplete (Wikipedia)
https://en.wikipedia.org/wiki/Autocomplete - Seriál Textový editor Vim jako IDE (zde na Root.cz)
https://www.root.cz/serialy/textovy-editor-vim-jako-ide/ - Jedi: A completion library for Python
https://www.masteringemacs.org/article/jedi-completion-library-python - Jedi.el – Python auto-completion for Emacs (GitHub)
https://github.com/tkf/emacs-jedi - Jedi.el – Python auto-completion for Emacs (dokumentace, popis možností apod.)
http://tkf.github.io/emacs-jedi/latest/ - Vim – the ubiquitous text editor
https://www.vim.org/ - GNU Emacs
https://www.gnu.org/software/emacs/ - EmacsWiki
https://www.emacswiki.org/emacs/EmacsWiki - Atom (stránka textového editoru)
https://atom.io/ - el-get (GitHub)
https://github.com/dimitri/el-get - MELPA
https://www.emacswiki.org/emacs/MELPA - Atom IDE
https://ide.atom.io/ - Sublime Text as Python IDE – jedi
https://screamingatmyscreen.com/2013/9/sublime-text-as-python-ide-jedi/