001.03 自建 Python 庫 Help

Jason990420發表於2020-01-17

檔案建立日期: 2020/01/17
最後修訂日期: 2020/01/19 新增右鍵移動功能
相關軟體資訊:

Win 10 Python 3.8.1 PySimpleGUI 4.15.1

參考檔案: None

說明: 所有內容歡迎引用, 只需註明來源及作者, 本文內容如有錯誤或用詞不當, 敬請指正.

標題: 001.03 自建Python庫Help

很多時候, 我們在編寫軟體時, 常常要去查閱相關的資料, 比如庫的說明書或上網查用法, 甚至要看下庫的原始碼, 還有相應的引數及其意義等等, 找起來很花時間, 尤其是不知道該使用的類或方法的名稱, 當然在IDE中輸入時會有提示, 但也要有輸入才會有相應的說明出來. 不管怎樣, 總是很浪費時間, 所以就順手寫了一個整理庫原始碼內容供查詢的軟體, 主要針對類和方法的定義及其中的說明字串.

首先, 必須自己確認哪些原始碼檔案有該庫的主要程式碼, 供軟體載入(LOAD). 絶大部份的庫原始碼都在Python安裝目錄的子目錄LIB, 或LIB子目錄site-packages中, 如果你看過某些庫的原始碼, 肯定很清楚這一點.

載入後可以在該項右鍵單擊, 更改名稱, 原預設名為原始碼的主要檔名, 如果有不需要的部份也可以右鍵單擊刪除. 載入的速度是很快的, 點選每一個專案, 右邊會顯示相的訊息以供參考.

輸出畫面

001.03 自建Python庫Help

程式碼說明

  1. 內容說明:
"""
Python Help File - Jason Yang 2019/01/17

Create Book to store major information from source code of Python packages.
It can help to view class or method quickly after you load the sourc codes.

Function Key:
    1. Buttons to load source code and sort all items
       (Should find the source code with major content of package)
    2. Right mouse button to rename or delete node.

Information will be shown:
    1. Class definition (class)
    2. Method definition (def)
    3. Document string (if soure code is well documented)

Note: All source codes should be with correct syntax.
"""
  1. 載入庫
    • token, tokenize 用來解析Python檔 ( .py )
    • PySimpleGUI 用來提供GUI介面
import sys
from token import *
from tokenize import tokenize
from pathlib import Path
import PySimpleGUI as sg
import ctypes
  1. 載入解析Python檔, 並建立樹的資料, 看到名稱, 非字串的’class’或’def’, 就取到’:’為完整的一行, 看到字串的”””就是檔案說明字串, 附加到前一行的說明內容.
def load(path, treedata):
    """
    Load Python file and parsing tokens.
    Capture two statements into treedata
    1. class name and text (definition, append document string)
    2. def name and text (definition, append document string)
    """
    try:
        with open(path, 'rb') as f:
            tokens = list(tokenize(f.readline))     # Covert .py into tokens
    except:
        sg.Popup('File Error or bad Python file!')  # Failed for open/read or
        return Fail                                 # sytax error.

    filename = Path(path).stem                      # Get filename
    key = find_key(treedata)                        # Create a unique key
    treedata.Insert('', key, filename, values=['']) # filename for root node

    key_list = [key]
    i = 0
    while i < len(tokens):                          # Search all tokens

        word = tokens[i].string

        # Name of 'class' or 'def' for statment
        if tok_name[tokens[i].type]=='NAME' and word in ['class', 'def']:
            level = tokens[i].start[1]//4           # indent for level

            if level > len(key_list)-1:             # if indent too much
                level = len(key_list)-1             # set to just one indent

            name = word+' '+tokens[i+1].string      # name for tree structure
            i += 2
            string = name

            while i < len(tokens):                  # find end of class or def

                char = tokens[i].string
                string += char

                if char == ',':                     # extra space for ','
                    string += ' '

                elif tok_name[tokens[i].exact_type]=='COLON':
                    key_list = key_list[:level+1]   # End of statement ':'
                    key = find_key(treedata)        # New key
                    key_list += [key, ]
                    treedata.Insert(                # Insert one node of tree
                        key_list[level], key, name, values=[string])
                    break

                i += 1

        elif tok_name[tokens[i].type]=='STRING' and word.startswith('"""'):

            level = tokens[i].start[1]//4           # Document string

            if level > len(key_list)-1:
                level = len(key_list)-1

            t = treedata.tree_dict[key_list[level]].values[0]   # append text
            t += '\n' + word[3:-3]
            t = t.replace('\r\n', '\n').replace('\n\n', '\n').strip()
            treedata.tree_dict[key_list[level]].values[0] = t   # update text

        i += 1

    return True
  1. 借用GregSal的程式碼, 提供可以Tree在右鍵時先選該項, 再執行原右鍵的功能.
class TreeRtClick(sg.Tree):
    '''
    GregSal commented on 24 Nov 2019
    https://github.com/PySimpleGUI/PySimpleGUI/issues/2234

    A variation of the Tree class that replaces the right-click callback
    function with one that first selects the tree item at the location of the
    right-click.
    '''
    def _RightClickMenuCallback(self, event):
        '''
        Replace the parent class' right-click callback function with one that
        first selects the tree item at the location of the right-click.
        '''
        tree = self.Widget  # Get the Tkinter Tree widget

        # These two calls are directly to the Tkinter Treeview widget.
        item_to_select = tree.identify_row(event.y) # Identify the tree item
        tree.selection_set(item_to_select)  # Set that item as selected.

        # Continue with normal right-click menu function.
        super()._RightClickMenuCallback(event)
  1. 繼承原PySimpleGUI的TreeData, 附加一些額外的方法, 如刪除, 移動, 更名, 存檔檔案, 讀檔案等.
class Tree_Data(sg.TreeData):
    """
    New Class for TreeData to create new methods
    move - Move node to under another node
    delete - Delete any node from tree
    save - Save treedata to 'Book.txt' file for using on next time.
    read - Read file 'Book.txt' file when script loading
    sort - Sord all node by name, __init__ first for more detail information
    """
    def __init__(self):

        super().__init__()
  • 移動樹中的節點, 首先在Node1右鍵選Move, 再選另一個Node2.
    def move(self, key1, key2):
        """
        Move node1/key1 to under node2/key2
        It will be failed if node2 is under node1
        """
        if key1 == '' and not self.is_legal_move(key1, key2):
            return False
        node = self.tree_dict[key1]
        parent1_node = self.tree_dict[node.parent]
        parent1_node.children.remove(node)      # removed from parent node
        parent2_node = self.tree_dict[key2]
        parent2_node.children.append(node)      # added into node2 children
        node.parent = parent2_node.key          # set node1.parent to node2
        return True
  • 刪除樹中的節點
    def delete(self, key):
        """
        Delete node/key and all nodes under it
        """
        if key == '':
            return False
        node = self.tree_dict[key]
        node_list = [node, ]
        parent_node = self.tree_dict[node.parent]
        parent_node.children.remove(node)           # Removed node from parent
        while node_list != []:                      # delete all nodes under it
            temp = []
            for item in node_list:
                temp += item.children
                del item
            node_list = temp
        return True
  • 檢查不會有上節點移到該下節點的情形, 該情形是不允許的
    def is_legal_move(self, key1, key2):
        """
        Check if node2/keys is under node1/key1
        """
        node_list = [self.tree_dict[key1], ]
        node2 = self.tree_dict[key2]

        while node_list != []:
            if node2 in node_list:
                return False
            temp = []
            for node in node_list:
                temp += node.children
            node_list = temp

        return True
  • 儲存樹資料, 供下次使用, 原樹資料都是以類來儲存, 必須轉換成其他資料格式, 如字典, 再轉字串存文字檔案.
    def save(self, filename):
        """
        Save all required data in treedata to file by dictionary
        Dictionary with key: [parent, children, text and values]
        Saved by str(dictionary) method
        """
        dict = {}
        for key, node in self.tree_dict.items():
            children = [n.key for n in node.children]
            dict[key]=[node.parent, children, node.text, node.values]

        with open(filename, 'wt', encoding='utf-8') as f:
            f.write(str(dict))
  • 讀入已有的資料檔案, 並建立樹結點, 從根節點的子節點往下延伸到末端, 直到整個樹建立.
    def read(self, filename):
        """
        Read treedata from file
        Insert Nodes from parent.children to parent, start from root
        """
        f = Path(filename)
        if not f.is_file():
            return
        with open(filename, 'rt', encoding='utf-8', newline='\n') as f:
            dict = eval(f.read())
        children = dict[''][1]
        while children != []:
            temp = []
            for child in children:
                node = dict[child]
                self.Insert(node[0], child, node[2], node[3])
                temp += node[1]
            children = temp
  • 對所有的節點以其名稱來排序, 因為大都時候def __init__是最基本的, 所以排在最前面, 再來是大寫字母, 小寫字母, 最後是其他底線開頭的在最後.
    def sort(self):
        """
        Sort children list of all nodes by name.

        The order is __init__, uppercase letter, lowercase letter, then '_'
        """
        def convert(text):
            temp = text.replace('__init__', ' ')    # Set __init__ lowest
            temp = temp.replace('_', '|')           # Set '_' to highest
            return temp

        for key, node in self.tree_dict.items():
            children = node.children
            node.children = sorted(                 # sort children by text
                children, key=lambda child: convert(child.text))
  1. PySimpleGUI的類或方法, 因為要設定相關的引數, 如顏色, 字型, 大小, 內容等等, 通常都有很多的引數, 很長的一行, 放在排版中, 不容易看排版的內容, 所有另外定義少引數的函式, 其他引數都當作預設值
def Button(Key):
    # Easy method for sg.Button
    return sg.Button(Key, enable_events=True, size=(10, 1), font=Font)


def Browse(Key):
    # Easy method for sg.FileBrowse
    return sg.FileBrowse(button_text=Key, target=Key, key=Key,
                size=(10, 1), font=Font, pad=(0, 0), enable_events=True,
                file_types=(("ALL Python Files", "*.py"),))


def Text(Key):
    # Easy method for sg.Multiline
    return sg.Multiline(default_text='', size=(120,33), enable_events=True,
                do_not_clear=True, key='Text', border_width=0, focus=False,
                font=Font, pad=(0, 0), text_color='white',
                background_color='blue')


def Tree(treedata):
    # Easy method for sg.Tree, here use class with modified method
    return TreeRtClick(
        data=treedata, headings=['Notes',], show_expanded=False, pad=(0, 0),
        col0_width=30, auto_size_columns=False, visible_column_map=[False,],
        select_mode=sg.TABLE_SELECT_MODE_BROWSE, enable_events=True,
        background_color='white', font=Small_Font, num_rows=33, row_height=23,
        key='Tree', right_click_menu=['Right',['Rename', 'Delete', 'Move']])
  1. 每一個樹的結點都需要一個獨立的Key, 所以從’1’, ‘2’, ….往上編碼, 每要一個新Key, 就從樹結點字典的Key中找一個最小且不存在的Key, 供新結點的使用.
def find_key(treedata):
    """
    Find a unique Key for new node, Key start from '1' and not in node list
    """
    i = 0
    while True:
        i += 1
        if str(i) not in treedata.tree_dict:
            return str(i)
  1. 主程式 - 建立GUI的佈局
ctypes.windll.user32.SetProcessDPIAware()       # Set unit of GUI to pixels
Font = ('Courier New', 12, 'bold')
Small_Font =  ('Courier New', 11, 'bold')

treedata = Tree_Data()                          # Initial root node of Tree
treedata.read('Book.txt')                       # Load all treedata from file

Layout = [[Browse('Load'), Button('Sort'), Button('Quit')],
          [Tree(treedata),Text('Text')]]
sg.theme('DarkPurple4')
Window = sg.Window('Python Book', layout=Layout, font=Font, finalize=True)
Window['Tree'].Widget.configure(show='tree')    # Invisiable Header
Window['Tree'].bind('<Button-1>', 'Down')       # add Button-1 event to Tree
Window['Text'].Widget.configure(wrap=None)      # Set Text no wrap
  1. 事件處理迴圈
Mode = Key1 = Key2 = None

while True:

    Event, Values = Window.read()

    if Event == None or Event == 'Quit':        # End of Script
        break

    elif Event == 'Load':                       # Load from Python library file
        path = Values['Load']
        if path.lower().endswith('.py'):
            load(path, treedata)
            Window['Tree'].Update(values=treedata)
            Window.Refresh()

    elif Event == 'Tree':                       # Update view on Text
        if Values['Tree']==[]:
            continue
        key = Values['Tree'][0]
        text = treedata.tree_dict[key].values[0]
        Window['Text'].Update(text)
        if Mode == 'Move':
            Key2 = key
            treedata.move(Key1, Key2)
            Window['Tree'].Update(values=treedata)
            Window['Text'].Update('')
            Mode = Key1 = Key2 = None

    elif Event == 'Rename':                     # Rename text name of node
        if Values['Tree']==[]:
            continue
        key = Values['Tree'][0]
        text = treedata.tree_dict[key].text
        t = sg.popup_get_text(message=f'Replace "{text}" with', font=Font,
            default_text=text, size=(40,1), no_titlebar=True, keep_on_top=True)
        if t != None:
            t = t.strip()
            treedata.tree_dict[key].text = t
            Window['Tree'].Update(key=key, text=t)

    elif Event == 'Delete':                     # Delete Node
        if Values['Tree']==[]:
            continue
        key = Values['Tree'][0]
        treedata.delete(key)
        Window['Tree'].Update(values=treedata)
        Window['Text'].Update('')

    elif Event == 'Move':                       # Move Node to
        if Values['Tree']==[]:                  # next clicked node
            continue
        Mode = 'Move'
        Key1 = Values['Tree'][0]
        sg.Popup('Click another label for destination.', font=Font)

    elif Event == 'Sort':                       # Sort all nodes by text
        treedata.sort()
        Window['Tree'].Update(values=treedata)
        Window['Text'].Update('')

treedata.save('Book.txt')                       # Save treedata before quit
del treedata                                    # deltete treedata
Window.close()
本作品採用《CC 協議》,轉載必須註明作者和本文連結
Jason Yang

相關文章