Hlavní navigace

Knihovna Jedi: doplňování kódu a statická analýza kódu v Pythonu

21. 8. 2018
Doba čtení: 24 minut

Sdílet

V dnešním článku se seznámíme s knihovnou Jedi určenou pro implementaci automatického doplňování kódu i pro statickou analýzu zdrojových kódů psaných v Pythonu. Popíšeme si i plugin jedi-vim integrující Jedi do Vimu.

Obsah

1. Použití knihovny Jedi pro automatické doplňování kódu a statickou analýzu zdrojových kódů Pythonu

2. Instalace knihovny Jedi a otestování korektnosti instalace

3. Využití možností nabízených knihovnou Jedi v interpretru Pythonu (REPL)

4. Základní funkce Jedi – automatické doplňování

5. Atributy objektů představujících návrhy na doplnění zdrojového textu

6. Dokumentační řetězce u návrhů doplnění zdrojového textu

7. Podpora pro skoky na deklarace funkcí, tříd nebo metod

8. Použití metody Script.goto_definitions()

9. Získání podrobnějších informací o deklarované funkci, třídě nebo metodě

10. Problematika redefinice funkcí v Pythonu

11. Dynamické chování Pythonu a jeho vliv na zjišťování informací o volaných funkcích

12. Složitější příklad ukazující dynamické chování Pythonu

13. Detekce míst ve zdrojovém kódu, v němž je nějaká funkce použita

14. Složitější příklad s redeklarací funkcí

15. Využití knihovny Jedi v pluginu jedi-vim

16. Možnosti nabízené pluginem jedi-vim

17. Obsah druhé části článku

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

19. Odkazy na Internetu

1. Použití knihovny Jedi pro automatické doplňování kódu a statickou analýzu zdrojových kódů Pythonu

Jedním z velmi užitečných nástrojů určených především pro vývojáře používající programovací jazyk Python je knihovna nazvaná Jedi. Tato knihovna slouží ke statické analýze zdrojových kódů napsaných právě v Pythonu (buď verze 2.7, popř. verzí 3.4, 3.5 atd.), přičemž výsledky provedené statické analýzy je možné využít pro implementaci mnoha užitečných operací, které známe z většiny moderních integrovaných vývojových prostředí (IDE). Zejména se jedná o operaci automatického doplňování kódu (autocompletion), například jména funkce, metody, názvu proměnné nebo parametru funkce. Ovšem knihovna Jedi podporuje i některé další zajímavé operace, například dokáže zjistit místo ve zdrojovém kódu, v němž je funkce/metoda/proměnná definována, místa v kódu, kde všude je daný objekt použit, nepřímo dokáže detekovat parametry funkcí a metod, apod.

Obrázek 1: Logo knihovny Jedi.

Vzhledem k potenciální užitečnosti knihovny Jedi nebude příliš velkým překvapením zjištění, že tuto knihovnu nalezneme jako součást pluginů určených pro programátorské textové editory. To se týká zejména Vimu, pro který existují pluginy jedi-vim, YouCompleteMe apod. postavené právě na Jedi (první z nich si ostatně dnes popíšeme v závěrečné části článku). Dále nesmíme zapomenout na textový editor Emacs s pluginy Jedi.el, elpy (Emacs Python Development Environment), populární Atom s pluginem pojmenovaným autocomplete-python-jedi, prostředí Visual Studio CodePython Extension, Eric IDE apod. Knihovna Jedi je taktéž použita v populárním IPythonu, blíže viz New completion API and Interface a dokonce můžeme její možnost automatického doplňování kódu použít i v běžném interpretu Pythonu, což si popíšeme ve třetí kapitole.

2. Instalace knihovny Jedi a otestování korektnosti instalace

V první polovině článku si na několika demonstračních příkladech ukážeme veřejné API knihovny Jedi. Ještě předtím je však samozřejmě nutné provést instalaci této knihovny, což ve skutečnosti není nic těžkého, protože můžeme (podobně jako u většiny knihoven a nástrojů popisovaných v předchozích článcích věnovaných Pythonu) použít nástroj pip2 nebo pip3 (Python Package Installer). Instalaci provedeme s využitím parametru –user lokálně pro přihlášeného uživatele, takže nainstalovanou knihovnu nalezneme v adresáři ~/.local/lib/pythonX.Y/ (kde X.Y je verze Pythonu 2 či Pythonu 3).

Příkaz pro instalaci Jedi ve variantě pro Python 3 a vlastní průběh instalace bude vypadat následovně:

$ pip3 install --user jedi
 
Collecting jedi
  Downloading https://files.pythonhosted.org/packages/3d/68/8bbf0ef969095a13ba0d4c77c1945bd86e9811960d052510551d29a2f23b/jedi-0.12.1-py2.py3-none-any.whl (174kB)
    100% |████████████████████████████████| 184kB 1.6MB/s
Collecting parso>=0.3.0 (from jedi)
  Downloading https://files.pythonhosted.org/packages/09/51/9c48a46334be50c13d25a3afe55fa05c445699304c5ad32619de953a2305/parso-0.3.1-py2.py3-none-any.whl (88kB)
    100% |████████████████████████████████| 92kB 2.2MB/s
Installing collected packages: parso, jedi
Successfully installed jedi-0.12.1 parso-0.3.1

Korektnost instalace si můžeme velmi snadno ověřit. Nejprve spustíme interpret Pythonu. Pokud jste pro instalaci použili příkaz pip2, spustíte samozřejmě interpret Pythonu 2.x, v případě použití příkazu pip3 pak interpret Pythonu 3.x:

$ python3
 
Python 3.6.3 (default, Oct  9 2017, 12:11:29)
[GCC 7.2.1 20170915 (Red Hat 7.2.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Následně se po zobrazení výzvy (prompt) pokusíme naimportovat hlavní modul knihovny Jedi:

>>> import jedi

V případě, že předchozí příkaz skončil bez chyby, můžeme se například pokusit vypsat si nápovědu (dokumentační řetězec) k této knihovně:

>>> help("jedi")
 
Help on package jedi:
 
NAME
    jedi
 
DESCRIPTION
    Jedi is a static analysis tool for Python that can be used in IDEs/editors. Its
    historic focus is autocompletion, but does static analysis for now as well.
    Jedi is fast and is very well tested. It understands Python on a deeper level
    than all other static analysis frameworks for Python.
 
    Jedi has support for two different goto functions. It's possible to search for
    related names and to list all names in a Python file and infer them. Jedi
    understands docstrings and you can use Jedi autocompletion in your REPL as
    well.
 
    Jedi uses a very simple API to connect with IDE's. There's a reference
    implementation as a `VIM-Plugin <https://github.com/davidhalter/jedi-vim>`_,
    which uses Jedi's autocompletion.  We encourage you to use Jedi in your IDEs.
    It's really easy.

Pokud nastane situace, že knihovna nebyla nainstalována, nebo došlo při instalaci k nějakým dalším problémům (špatně zadané cesty atd.), vypíše se po pokusu o import tato chyba:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named 'jedi'

3. Využití možností nabízených knihovnou Jedi v interpretru Pythonu (REPL)

Již v úvodní kapitole jsme si řekli, že knihovnu Jedi, přesněji řečeno její část umožňující automatické doplňování kódu (jména metod a funkcí), je možné použít i ve standardním interpretru Pythonu. Ukažme si nyní, jak lze tuto užitečnou vlastnost do interpretru vlastně přidat. Nejdříve je nutné nastavit proměnnou prostředí nazvanou PYTHONSTARTUP, do které je možné zapsat jméno souboru s inicializačním skriptem (blíže viz oficiální dokumentace k Pythonu). Nastavení by pro knihovnu Jedi mělo vypadat následovně:

$ export PYTHONSTARTUP="$(python3 -m jedi repl)"

Dále již můžeme běžným způsobem spustit interpret Pythonu. Povšimněte si ovšem posledního řádku zobrazeného před výzvou (prompt). Tento řádek nám oznamuje, že se knihovna Jedi inicializovala a bude použita při zadávání příkazů:

$ python3
 
Python 3.6.3 (default, Oct  9 2017, 12:11:29)
[GCC 7.2.1 20170915 (Red Hat 7.2.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
REPL completion using Jedi 0.12.1
>>>

Pokusme se nyní využít automatické doplňování kódu nabízené touto knihovnou. Nejdříve (čistě jen pro příklad) naimportujeme standardní knihovnu time:

>>> import time

Následně zadáme část příkazu time.asctime().is a namísto doplnění celého jména funkce stlačíme klávesu [Tab]. V tomto okamžiku je knihovně Jedi předán zapsaný skript (více viz navazující kapitoly) a knihovna Jedi vrátí seznam metod objektu typu str, které začínají na prefix „is“:

>>> time.asctime().is
...alnum       ...digit       ...numeric     ...title
...alpha       ...identifier  ...printable   ...upper
...decimal     ...lower       ...space
Poznámka: povšimněte si, že se skutečně nabízí pouze názvy metod, které mají v daném kontextu význam. Například se nenabízí standardní funkce isinstance nebo issubclass, protože ty nedávají pro návratovou hodnotu funkce time.asctime() žádný rozumný význam.

4. Základní funkce Jedi – automatické doplňování

Nyní si popíšeme základní koncepty, na nichž je postaveno veřejné API knihovny Jedi. Ukažme si nejprve, jakým způsobem je možné funkci pro automatické doplňování kódu (jmen funkcí atd.) ukázanou prakticky v předchozí kapitole využít programově, tj. v běžném skriptu naprogramovaném přímo v Pythonu. Přitom si vysvětlíme základní třídu Script, na níž je postavena základní část API knihovny Jedi.

Nejprve provede import knihovny:

import jedi

Následně deklarujeme řetězec src reprezentující zdrojový kód, nad nímž budeme automatické doplňování spouštět. Pro jednoduchost bude tento kód reprezentován přímo v těle příkladu formou víceřádkového řetězce, tedy následovně (v praxi by tento řetězec posílal přímo textový editor nebo IDE):

src = '''
anybody=True
answer="42"
an'''
Poznámka: povšimněte si, že první řádek je prázdný (za trojicí otevíracích apostrofů ''' následuje konec řádku) a taktéž toho, že poslední řádek vlastně neobsahuje platný příkaz Pythonu. To knihovně Jedi v žádném případě nevadí, ostatně i všechna běžná IDE se musí vypořádat s tím, že v průběhu editace nepracují s validním kódem.

Následuje nejdůležitější část celého skriptu – předání zdrojového kódu knihovně Jedi a určení, na kterém místě se nachází pomyslný kurzor, protože právě toto místo bude knihovna Jedi při analýze považovat za klíčové pro funkci automatického doplňování. Pozice kurzoru je vyjádřena dvěma celými čísly – číslem řádku a číslem sloupce, přičemž čísla řádků začínají od jedničky (tak je tomu ostatně i v textových editorech). Číslo sloupce nemusíme složitě počítat, ale použijeme funkci len():

     1
     2  anybody=True
     3  answer="42"
     4  an[pozice kurzoru vypočtená funkcí len()]

Posledním parametrem předávaným konstruktoru třídy Script je jméno modulu, ale to můžeme ponechat prázdné (prozatím ho nevyužijeme):

Zavolání konstruktoru třídy Script() tedy bude vypadat následovně:

script = jedi.Script(src, 4, len('an'), '')

Metoda pro zjištění všech možností, které nám prefix „an“ nabízí, se zavolá následujícím způsobem:

print(script.completions())

Metoda Script.completions() by měla vrátit seznam obsahující tři objekty typu Completion. Tyto objekty představují texty „answer“, „any“ a „anybody“, tj. všechny možné identifikátory, které mají v kontextu zdrojového kódu smysl („answer“ a „anybody“ je deklarován přímo v analyzovaném zdrojovém kódu, „any“ je naproti tomu standardní vestavěná funkce Pythonu):

[<Completion: answer>, <Completion: any>, <Completion: anybody>]

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 = '''
anybody=True
answer="42"
an'''
 
script = jedi.Script(src, 4, len('an'), '')
print(script.completions())

5. Atributy objektů představujících návrhy na doplnění zdrojového textu

V předchozí kapitole jsme si řekli, že metoda Script.completions() nevrací pouze seznam textů (řetězců), které v daném místě kódu dávají smysl pro doplnění. To by sice mohlo dostačovat, ale pouze pro ty nejjednodušší IDE. V moderních IDE se většinou očekává, že technologie pro automatické doplňování kódu zobrazí i další relevantní údaje, například kde je definován identifikátor, který je nabízen pro automatické doplnění atd. A právě tyto údaje je možné získat z objektů typu Completion, které jsou metodou Script.completions() vráceny.

Tyto objekty obsahují atribut nazvaný complete, který obsahuje pouze text, který se má doplnit za již zapsaný text. V našem ukázkovém skriptu se po zadání prefixu „an“ vrátí tři objekty s těmito atributy complete:

swer
y
ybody

Naproti tomu atribut name obsahuje plné jméno pro doplnění, tedy včetně zapsaného prefixu:

answer
any
anybody

Díky existenci těchto dvou atributů je implementace technologie pro automatické doplňování do IDE či textových editorů jednodušší.

Opět se podívejme na demonstrační příklad, který vypíše obsah obou výše popsaných atributů pro všechny získané objekty typu Completion:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
anybody=True
answer="42"
an'''
 
script = jedi.Script(src, 4, len('an'), '')
 
completions = script.completions()
 
for completion in completions:
    print(completion.complete)

print()
 
for completion in completions:
    print(completion.name)

6. Dokumentační řetězce u návrhů doplnění zdrojového textu

Další informací obsaženou v objektech typu Completion je dokumentační řetězec, který je ovšem pochopitelně vyplněn pouze ve chvíli, kdy je v analyzovaných zdrojových kódech nalezen. Dokumentační řetězec lze získat zavoláním metody docstring() (nikoli atributu docstring), což je ukázáno v dnešním třetím demonstračním příkladu, v němž se bude funkce pro automatické doplnění kódu spouštět nad skriptem obsahujícím i funkci s dokumentačním řetězcem:

     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  anybody=True
    14  answer="42"
    15  an[pozice kurzoru pro automatické doplňování]

Úplný zdrojový kód tohoto příkladu vypadá následovně:

#!/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
 
anybody=True
answer="42"
an'''
 
lines = src.count('\n')
script = jedi.Script(src, lines+1, len('an'), '')
 
completions = script.completions()
 
for completion in completions:
    print(completion.name)
    print("-"*40)
    print(completion.docstring())
    print("\n"*3)

Výsledek bude vypadat následovně. Povšimněte si, že dokumentační řetězec není vypsán u proměnných, u nichž není definován:

anagrams
----------------------------------------
anagrams(word)
 
Very primitive anagram generator.
 
 
 
 
answer
----------------------------------------
 
 
 
 
 
any
----------------------------------------
Return True if bool(x) is True for any x in the iterable.
 
If the iterable is empty, return False.
 
 
 
 
anybody
----------------------------------------

7. Podpora pro skoky na deklarace funkcí, tříd nebo metod

V knihovně Jedi nalezneme i další užitečnou funkcionalitu. Jedná se zejména o zjištění podrobnějších informací o volané funkci nebo metodě. Jméno funkce/metody je opět získáno na místě, kde se nachází pomyslný kurzor definovaný číslem řádku a sloupce. Pro tento účel se používá metoda Script.goto_definitions(), která vrací seznam objektů typu Definition (skutečně se opět jedná o seznam, důvod bude vysvětlen v navazujících kapitolách). Samotná třída Definition je popsaná na stránce https://jedi.readthedocs.i­o/en/latest/docs/api-classes.html#jedi.api.clas­ses.Definition; její použití si ukážeme na několika demonstračních příkladech zmíněných níže.

8. Použití metody Script.goto_definitions()

V dalším demonstračním příkladu si ukážeme použití metody Script.goto_definitions() pro získání podrobnějších informací o funkcích x a y volaných na řádcích 5 a 6 (v obou případech se jedná o sloupec 7, kde začíná volání funkce):

     1
     2  def x():
     3      return 42
     4
     5  print(x())
     6  print(y())

Informace o těchto funkcích lze přečíst následujícím způsobem (povšimněte si použití jména skriptu „example.py“, které může být vymyšleno a typicky ho knihovně Jedi dodá textový editor):

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
def x():
    return 42

print(x())
print(y())
'''
 
script = jedi.Script(src, 5, 7, 'example.py')
 
goto_definitions = script.goto_definitions()
print(goto_definitions)
 
print("-"*40)
 
script = jedi.Script(src, 6, 7, 'example.py')
 
goto_definitions = script.goto_definitions()
print(goto_definitions)

Výsledkem bude v prvním případě jednoprvkový seznam obsahující (jediný) objekt typu Definition. V případě druhém se vrátí prázdný seznam, protože knihovna Jedi žádné informace o funkci y nedokáže zjistit:

[<Definition def x>]
----------------------------------------
[]

9. Získání podrobnějších informací o deklarované funkci, třídě nebo metodě

Ukažme si ještě poněkud složitější příklad, v němž budeme zjišťovat informace o volaných funkcích i anonymních funkcích v následujícím skriptu (viz zvýrazněné pasáže):

     1
     2  def x():
     3      return 42
     4
     5  def y():
     6      return 42
     7
     8  print(x())
     9  print(y())
    10  print(z())
    11
    12  w = lambda: 42
    13
    14  print(w())

Každý objekt typu Definition obsahuje několik atributů s typem objektu (zda se jedná o funkci atd.), plným jménem objektu, jménem modulu a taktéž řádkem, na němž byl objekt definován. O výpis se postará tato pomocná funkce:

def print_definitions(source, line, column, module):
    print("-"*40)
 
    script = jedi.Script(source, line, column, module)
 
    goto_definitions = script.goto_definitions()
 
    if not goto_definitions:
        print("not found")
        return
 
    for definition in goto_definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))

Zjištění a zobrazení informací pro výše zvýrazněné pasáže zajistí tyto čtyři řádky:

script = print_definitions(src, 8, 7, 'example.py')
script = print_definitions(src, 9, 7, 'example.py')
script = print_definitions(src, 10, 7, 'example.py')
script = print_definitions(src, 14, 7, 'example.py')

S výsledky ukazujícími přesné místo deklarace příslušné funkce nebo lambda funkce:

----------------------------------------
function __main__.x in example.py:2
----------------------------------------
function __main__.y in example.py:5
----------------------------------------
not found
----------------------------------------
function __main__.<lambda> in example.py:12

Úplný zdrojový kód tohoto demonstračního příkladu vypadá následovně:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
def x():
    return 42
 
def y():
    return 42
 
print(x())  # line 8
print(y())  # line 9
print(z())  # line 10
 
w = lambda: 42
 
print(w())  # line 14
'''
 
 
def print_definitions(source, line, column, module):
    print("-"*40)
 
    script = jedi.Script(source, line, column, module)
 
    goto_definitions = script.goto_definitions()
 
    if not goto_definitions:
        print("not found")
        return
 
    for definition in goto_definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
script = print_definitions(src, 8, 7, 'example.py')
script = print_definitions(src, 9, 7, 'example.py')
script = print_definitions(src, 10, 7, 'example.py')
script = print_definitions(src, 14, 7, 'example.py')

10. Problematika redefinice funkcí v Pythonu

Co se stane ve chvíli, kdy se pokusíme funkci deklarovat vícekrát? To je v Pythonu samozřejmě dovoleno:

     1
     2  def x():
     3      return 1
     4
     5  def x():
     6      return 2
     7
     8  def x():
     9      return 3
    10
    11  print(x())

Pro volání funkce x na jedenáctém řádku se nyní vypíše, že se volá funkce deklarovaná na osmém řádku (což je v souladu s chováním samotného Pythonu):

----------------------------------------
function __main__.x in example.py:8

Předchozí kód demonstračního příkladu byl nepatrně upraven a nyní vypadá následovně:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
def x():
    return 1
 
def x():
    return 2
 
def x():
    return 3
 
print(x())
'''
 
 
def print_definitions(source, line, column, module):
    print("-"*40)
 
    script = jedi.Script(source, line, column, module)
 
    goto_definitions = script.goto_definitions()
 
    if not goto_definitions:
        print("not found")
        return
 
    for definition in goto_definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
script = print_definitions(src, 11, 7, 'example.py')

11. Dynamické chování Pythonu a jeho vliv na zjišťování informací o volaných funkcích

Víme již, že návratovou hodnotou metody Script.goto_definitions() je seznam objektů typu Definition. Proč tomu tak je si ukážeme na dalším testovacím skriptu:

     1
     2  if random.random() < 0.5:
     3      def x():
     4          return 1
     5  else:
     6      def x():
     7          return 2
     8
     9  print(x())

Při získání informací o funkci volané na devátém řádku získáme seznam se dvěma objekty Definition, protože knihovna Jedi nemůže ze statické analýzy kódu zjistit, která funkce se skutečně zavolá (resp. přesněji řečeno, která funkce je vůbec deklarována):

----------------------------------------
function __main__.x in example.py:3
function __main__.x in example.py:6

Opět si pro úplnost ukažme zdrojový kód tohoto příkladu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
if random.random() < 0.5:
    def x():
        return 1
else:
    def x():
        return 2
 
print(x())
'''
 
 
def print_definitions(source, line, column, module):
    print("-"*40)
 
    script = jedi.Script(source, line, column, module)
 
    goto_definitions = script.goto_definitions()
 
    if not goto_definitions:
        print("not found")
        return
 
    for definition in goto_definitions:
        print("{type} {name} in {module}.py:{line}".format(type=definition.type,
                                                           name=definition.full_name,
                                                           module=definition.module_name,
                                                           line=definition.line))
 
 
script = print_definitions(src, 9, 7, 'example.py')

12. Složitější příklad ukazující dynamické chování Pythonu

Předchozí příklad je možné upravit do ještě složitější podoby, tentokrát dokonce s lambda funkcí:

     1
     2  def x():
     3      return 1
     4
     5  def y():
     6      return 2
     7
     8  z = lambda: 3
     9
    10  if random.random() < 0.3:
    11      f = x
    12  else:
    13      if random.random() < 0.3:
    14          f = y
    15      else:
    16          f = z
    17
    18  print(f())

Pokud se pokusíme získat informaci o deklaraci funkce x volané na osmnáctém řádku, dostaneme tři objekty typu Definition; u posledního objektu dokonce s korektní informací o tom, že se jedná o lambda funkci:

----------------------------------------
function __main__.x in example.py:2
function __main__.y in example.py:5
function __main__.<lambda>; in example.py:8

Opět si pro úplnost ukažme zdrojový kód tohoto příkladu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
def x():
    return 1
 
def y():
    return 2
 
z = lambda: 3
 
if random.random() < 0.3:
    f = x
else:
    if random.random() < 0.3:
        f = y
    else:
        f = z
 
print(f())
'''
 
 
def print_definitions(source, line, column, module):
    print("-"*40)
 
    script = jedi.Script(source, line, column, module)
 
    goto_definitions = script.goto_definitions()
 
    if not goto_definitions:
        print("not found")
        return
 
    for definition in goto_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 = print_definitions(src, lines, 7, 'example.py')

13. Detekce míst ve zdrojovém kódu, v němž je nějaká funkce použita

V dalším příkladu si ukážeme jeden ze způsobů, jakým je možné získat seznam míst v analyzovaném zdrojovém kódu, v němž je nějaká funkce použita (tedy volána) nebo deklarována. Testovací skript, který budeme analyzovat, vypadá následovně:

     1
     2  def x():
     3      return 1
     4
     5  a = x()
     6  b = x() + x()
     7
     8  print(x())

Povšimněte si, že na čtyřech místech voláme funkci x, která je deklarována na řádku 2. Seznam deklarací a použití této funkce získáme metodou Script.usages(), tedy přibližně následujícím způsobem:

script = jedi.Script(source, line, column, module)
 
usages = script.usages()

Záleží jen na nás, jaký výskyt funkce x zvolíme při volání konstruktoru Script(). Může se jednat například o řádek 8, sloupec 7, tj. o poslední volání funkce x:

     1
     2  def x():
     3      return 1
     4
     5  a = x()
     6  b = x() + x()
     7
     8  print(x())

Výsledek:

function __main__.x in example.py:2
statement __main__.x in example.py:5
statement __main__.x in example.py:6
statement __main__.x in example.py:6
statement __main__.x in example.py:8

Ovšem naprosto stejnou informaci získáme ve chvíli, kdy budeme analyzovat řádek 2, sloupec 5, tj. deklaraci funkce:

     1
     2  def x():
     3      return 1
     4
     5  a = x()
     6  b = x() + x()
     7
     8  print(x())

Výsledek je v tomto případě shodný s výsledkem předchozím:

function __main__.x in example.py:2
statement __main__.x in example.py:5
statement __main__.x in example.py:6
statement __main__.x in example.py:6
statement __main__.x in example.py:8

Celý zdrojový kód, který tyto výsledky vypisuje, vypadá následovně:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
def x():
    return 1
 
a = x()
b = x() + x()
 
print(x())
'''
 
 
def print_usages(source, line, column, module):
    script = jedi.Script(source, line, column, module)
 
    usages = script.usages()
 
    if not usages:
        print("not found")
        return
 
    for usage in usages:
        print("{type} {name} in {module}.py:{line}".format(type=usage.type,
                                                           name=usage.full_name,
                                                           module=usage.module_name,
                                                           line=usage.line))
 
 
script = print_usages(src, 8, 7, 'example.py')
 
print()
 
script = print_usages(src, 2, 5, 'example.py')

14. Složitější příklad s redeklarací funkcí

Z předchozího příkladu by se mohlo zdát, že je úplně jedno, jaký výskyt funkce ve zdrojovém kódu vybereme – vždy by se měl (zdánlivě) vrátit stejný seznam použití a deklarací této funkce. Ve skutečnosti to však pochopitelně není zcela pravdivé, protože díky dynamické povaze Pythonu je možné funkci redeklarovat a zcela tak pozměnit její chování. Ostatně podívejme se na následující skript, v němž je funkce x deklarována dvakrát:

     1
     2  def x():
     3      return 1
     4
     5  print(x())
     6
     7  a = x()
     8  b = x() + x()
     9
    10  def x():
    11      return 2
    12
    13  print(x())

Pokud se zeptáme na použití funkce na řádku 5, sloupci 7:

     1
     2  def x():
     3      return 1
     4
     5  print(x())
     6
     7  a = x()
     8  b = x() + x()
     9
    10  def x():
    11      return 2
    12
    13  print(x())

Dostaneme výsledek odpovídající zhruba prvním dvou třetinám skriptu (až do druhé deklarace funkce x):

function __main__.x in example.py:2
statement __main__.x in example.py:5
statement __main__.x in example.py:7
statement __main__.x in example.py:8
statement __main__.x in example.py:8

Naproti tomu při analýze řádku 13, sloupce 7:

     1
     2  def x():
     3      return 1
     4
     5  print(x())
     6
     7  a = x()
     8  b = x() + x()
     9
    10  def x():
    11      return 2
    12
    13  print(x())

Bude výsledek zcela odlišný a bude plně odpovídat chování Pythonu:

function __main__.x in example.py:10
statement __main__.x in example.py:13

Tato užitečná vlastnost knihovny Jedi je ukázána v dnešním posledním příkladu, jehož úplný zdrojový kód je následující:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
import jedi
 
src = '''
def x():
    return 1
 
print(x())
 
a = x()
b = x() + x()
 
def x():
    return 2
 
print(x())
'''
 
 
def print_usages(source, line, column, module):
    script = jedi.Script(source, line, column, module)
 
    usages = script.usages()
 
    if not usages:
        print("not found")
        return
 
    for usage in usages:
        print("{type} {name} in {module}.py:{line}".format(type=usage.type,
                                                           name=usage.full_name,
                                                           module=usage.module_name,
                                                           line=usage.line))
 
 
script = print_usages(src, 5, 7, 'example.py')
 
print()
 
script = print_usages(src, 13, 7, 'example.py')
 
print()
 
script = print_usages(src, 2, 5, 'example.py')

15. Využití knihovny Jedi v pluginu jedi-vim

Podívejme se nyní na jeden ze způsobů praktického využití knihovny Jedi. Tato knihovna je totiž intenzivně používaná například přídavným modulem (pluginem) nazvaným jedi-vim, který je pochopitelně určený, jak již jeho název napovídá, pro textový editor Vim. Díky propojení Vimu s knihovnou Jedi je možné přímo z prostředí textového editoru používat prakticky všechny základní operace, na které jsme zvyklí z „plnohodnotných“ integrovaných vývojových prostředí (IDE). Zejména se jedná o automatické doplňování jmen funkcí, tříd, metod a proměnných, zobrazení dynamické nápovědy s parametry funkce či metody (u Pythonu bez uvedení typu), skok na deklaraci funkce, třídy, metody, zobrazení dokumentace k prakticky libovolnému objektu, nad nímž se nachází kurzor a konečně o operaci přejmenování (samozřejmě v míře použitelné pro tak dynamický jazyk, jakým Python bezesporu je – nečekejte zde stoprocentní funkčnost v míře obvyklé například pro Javu).

Obrázek 2: Pro bezproblémovou funkčnost pluginu jedi-vim je nutné, aby byl textový editor Vim přeložen s povolenou volbou +conceal. Existenci této volby zjistíme například příkazem :ver či :version.

Obrázek 3: Na tomto screenshotu je patrné, že volba +conceal byla na mém systému skutečně při překladu Vimu povolena.

Před popisem základních vlastností pluginu jedi-vim si samozřejmě musíme tento plugin nainstalovat. Ve Fedoře (ale i v dalších linuxových distribucích) je to jednoduché, neboť jedi-vim je většinou dostupný ve standardním repositáři distribuce a tím pádem i ve správci balíčků. Pro větší zmatek se však tento balíček někdy nejmenuje jedi-vim, ale naopak vim-jedi :-) Instalaci tedy provedeme buď z účtu superuživatele:

# dnf install vim-jedi

Nebo alternativně z účtu toho uživatele, který má příslušná práva nastavená v sudoers:

$ sudo dnf install vim-jedi

Samotná instalace je provedena prakticky okamžitě:

Last metadata expiration check: 1:19:11 ago on Fri 17 Aug 2018 05:30:26 AM EDT.
Dependencies resolved.
================================================================================
 Package          Arch      Version           Repository                   Size
================================================================================
Installing:
 vim-jedi         noarch    0.8.0-4.fc28      Fedora-Everything            28 k
Installing dependencies:
 python2-jedi     noarch    0.12.0-1.fc28     updates                     291 k
 python2-parso    noarch    0.2.1-1.fc28      updates                     142 k
 
Transaction Summary
================================================================================
Install  3 Packages
 
Total download size: 461 k
Installed size: 1.9 M
Is this ok [y/N]: y

Vidíme, že se v případě potřeby doinstaluje i samotná knihovna Jedi:

Downloading Packages:
(1/3): vim-jedi-0.8.0-4.fc28.noarch.rpm         508 kB/s |  28 kB     00:00
(2/3): python2-parso-0.2.1-1.fc28.noarch.rpm    375 kB/s | 142 kB     00:00
(3/3): python2-jedi-0.12.0-1.fc28.noarch.rpm    674 kB/s | 291 kB     00:00
--------------------------------------------------------------------------------
Total                                           524 kB/s | 461 kB     00:00
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                        1/1
  Installing       : python2-parso-0.2.1-1.fc28.noarch                      1/3
  Installing       : python2-jedi-0.12.0-1.fc28.noarch                      2/3
  Installing       : vim-jedi-0.8.0-4.fc28.noarch                           3/3
  Running scriptlet: vim-jedi-0.8.0-4.fc28.noarch                           3/3
  Verifying        : vim-jedi-0.8.0-4.fc28.noarch                           1/3
  Verifying        : python2-jedi-0.12.0-1.fc28.noarch                      2/3
  Verifying        : python2-parso-0.2.1-1.fc28.noarch                      3/3
 
Installed:
  vim-jedi.noarch 0.8.0-4.fc28           python2-jedi.noarch 0.12.0-1.fc28
  python2-parso.noarch 0.2.1-1.fc28
 
Complete!

16. Možnosti nabízené pluginem jedi-vim

Použití pluginu jedi-vim při editaci zdrojových kódů napsaných v Pythonu si nejlépe ukážeme na několika screenshotech:

Obrázek 4: Po instalaci pluginu jedi-vim je k dispozici i nápověda vyvolaná příkazem :help jedi-vim.

Obrázek 5: Základní funkcí pluginu jedi-vim je automatické doplňování kódu vyvolané klávesovou zkratkou Ctrl+Space ve vkládacím režimu.

Obrázek 6: Zobrazení dokumentačního řetězce v režimu automatického doplňování.

Obrázek 7: Nastavení zvýraznění skupiny jedifat v případě, že nápověda k parametrům funkcí není čitelná (což skutečně není ve chvíli, kdy je použit terminálu se světlým pozadím). Bližší informace o této vlastnosti Vimu lze najít v článku Textový editor Vim jako IDE (4.část) .

Obrázek 8: Zobrazení parametrů funkce se zvýrazněním právě zadávaného parametru.

Obrázek 9: Zobrazení parametrů funkce se zvýrazněním právě zadávaného parametru.

Obrázek 10: Zobrazení parametrů funkce se zvýrazněním právě zadávaného parametru.

Obrázek 11: 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.

root_podpora

17. Obsah druhé části článku

Ve druhé části tohoto článku popis možností nabízených knihovnou Jedi dokončíme. Ukážeme si další funkcionalitu dostupnou při statické analýze a taktéž si popíšeme možnosti nastavení a konfigurace virtuálního prostředí Pythonu, v jehož rámci je vlastně statická analýza prováděna.

18. 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 jedi01_simple_completions.py základní automatické doplnění kódu https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi01_simple_completi­ons.py
2 jedi02_completions_attributes.py atributy objektů Completion https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi02_completions_attri­butes.py
3 jedi03_completions_docstrings.py dokumentační řetězce v objektech Completion https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi03_completions_doc­strings.py
4 jedi04_goto_definitions.py získání základních informací o volaných funkcích https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi04_goto_definition­s.py
5 jedi05_goto_definitions_attributes.py získání podrobnějších informací o volaných funkcích https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi05_goto_definition­s_attributes.py
6 jedi06_function_redeclaration.py chování při opětovné deklaraci funkce https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi06_function_redecla­ration.py
7 jedi07_dynamic_behaviour_A.py dynamické chování Pythonu a vliv na knihovnu Jedi https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi07_dynamic_behaviou­r_A.py
8 jedi08_dynamic_behaviour_B.py dynamické chování Pythonu a vliv na knihovnu Jedi https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi08_dynamic_behaviou­r_B.py
9 jedi09_usages.py informace o místech použití funkcí nebo metod https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi09_usages.py
10 jedi10_usages_and_redeclarations.py informace o místech použití funkcí nebo metod https://github.com/tisnik/pre­sentations/blob/master/je­di/jedi10_usages_and_rede­clarations.py

19. 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.el – Python auto-completion for Emacs
    https://tkf.github.io/emacs-jedi/latest/

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.