สร้างสถาปัตยกรรมปลั๊กอินขั้นต่ำใน Python


190

ฉันมีแอปพลิเคชันเขียนด้วย Python ซึ่งใช้โดยผู้ชมทางเทคนิคที่ค่อนข้างเป็นธรรม (นักวิทยาศาสตร์)

ฉันกำลังมองหาวิธีที่ดีในการทำให้แอปพลิเคชันสามารถขยายได้โดยผู้ใช้เช่นสถาปัตยกรรมสคริปต์ / ปลั๊กอิน

ฉันกำลังมองหาบางสิ่งบางอย่างที่เบามาก สคริปต์หรือปลั๊กอินส่วนใหญ่จะไม่ได้รับการพัฒนาและแจกจ่ายโดยบุคคลที่สามและติดตั้ง แต่จะเป็นสิ่งที่ผู้ใช้ทำขึ้นภายในไม่กี่นาทีเพื่อให้งานที่ทำซ้ำเพิ่มการรองรับรูปแบบไฟล์โดยอัตโนมัติ เป็นต้นดังนั้นปลั๊กอินควรมีรหัสสำเร็จรูปขั้นต่ำที่แน่นอนและไม่จำเป็นต้องมี 'การติดตั้ง' นอกเหนือจากการคัดลอกไปยังโฟลเดอร์ (ดังนั้นบางอย่างเช่นจุดเข้า setuptools หรือสถาปัตยกรรมปลั๊กอิน Zope ดูเหมือนมากเกินไป)

มีระบบแบบนี้ออกมาแล้วหรือมีโครงการที่ใช้รูปแบบที่คล้ายกันซึ่งฉันควรมองหาความคิด / แรงบันดาลใจ?

คำตอบ:


150

โดยทั่วไปแล้วไดเรกทอรีของฉันที่เรียกว่า "ปลั๊กอิน" ซึ่งแอปหลักสามารถสำรวจความคิดเห็นแล้วใช้imp.load_moduleเพื่อรับไฟล์ค้นหาจุดเข้าที่รู้จักกันดีที่อาจมี Params config โมดูลในระดับและไปจากที่นั่น ฉันใช้สิ่งที่ตรวจสอบไฟล์สำหรับไดนามิคจำนวนหนึ่งซึ่งปลั๊กอินใช้งานอยู่ แต่นั่นเป็นสิ่งที่ดี

แน่นอนความต้องการใด ๆ ที่มาพร้อมกับการพูดว่า "ฉันไม่ต้องการ [สิ่งที่ใหญ่และซับซ้อน] X; ฉันแค่ต้องการบางสิ่งที่มีน้ำหนักเบา" จะเสี่ยงต่อการใช้ X หนึ่งอีกครั้งในเวลาที่ต้องการ แต่นั่นไม่ใช่การบอกว่าคุณไม่สามารถสนุกกับมันได้ :)


26
ขอบคุณมาก! ฉันเขียนแบบฝึกหัดเล็กน้อยตามโพสต์ของคุณ: lkubuntu.wordpress.com/2012/10/02/writing-a-python-plugin-api
MiJyn

9
impโมดูลจะถูกเลิกในความโปรดปรานของimportlibเริ่มต้นจากหลาม 3.4
b0fh

1
ในหลายกรณีการใช้งานคุณสามารถใช้importlib.import_moduleimp.load_moduleแทนสำหรับ
Chris Arndt

58

module_example.py:

def plugin_main(*args, **kwargs):
    print args, kwargs

loader.py:

def load_plugin(name):
    mod = __import__("module_%s" % name)
    return mod

def call_plugin(name, *args, **kwargs):
    plugin = load_plugin(name)
    plugin.plugin_main(*args, **kwargs)

call_plugin("example", 1234)

แน่นอนว่า "น้อยที่สุด" ไม่มีการตรวจสอบข้อผิดพลาดอย่างแน่นอนอาจมีปัญหาด้านความปลอดภัยนับไม่ถ้วนมันไม่ยืดหยุ่นมาก - แต่มันควรจะแสดงให้คุณเห็นว่าระบบปลั๊กอินใน Python นั้นง่ายเพียงใด ..

คุณอาจต้องการที่จะดูเป็นเด็กซนโมดูลเกินไปถึงแม้ว่าคุณจะสามารถทำมากมีเพียง__import__, os.listdirและบางส่วนจัดการสตริง


4
ฉันคิดว่าคุณอาจต้องการที่จะเปลี่ยนdef call_plugin(name, *args)ไปdef call_plugin(name, *args, **kwargs)แล้วplugin.plugin_main(*args)ไปplugin.plugin_main(*args, **kwargs)
รอนไคลน์

12
ใน python 3 impเลิกใช้แล้วimportlib
Adam Baxter


25

ในขณะที่คำถามนั้นน่าสนใจจริง ๆ ฉันคิดว่ามันค่อนข้างยากที่จะตอบโดยไม่มีรายละเอียดเพิ่มเติม แอปพลิเคชันประเภทนี้คืออะไร? มันมี GUI หรือไม่? มันเป็นเครื่องมือบรรทัดคำสั่งหรือไม่? ชุดของสคริปต์? โปรแกรมที่มีจุดเข้าใช้งานที่ไม่เหมือนใคร ฯลฯ ...

เมื่อได้รับข้อมูลเล็ก ๆ น้อย ๆ ที่ฉันมีฉันจะตอบในลักษณะทั่วไปมาก

คุณต้องเพิ่มปลั๊กอินหมายความว่าอย่างไร

  • คุณอาจจะต้องเพิ่มไฟล์กำหนดค่าซึ่งจะแสดงรายการพา ธ / ไดเรกทอรีที่จะโหลด
  • อีกวิธีหนึ่งก็คือจะพูดว่า "ไฟล์ใด ๆ ในปลั๊กอิน / ไดเรกทอรีนั้นจะถูกโหลด" แต่มีความไม่สะดวกที่จะทำให้ผู้ใช้ของคุณต้องย้ายไฟล์ต่างๆ
  • ตัวเลือกสุดท้ายที่เป็นสื่อกลางคือต้องการให้ปลั๊กอินทั้งหมดอยู่ในปลั๊กอิน / โฟลเดอร์เดียวกันจากนั้นจึงเปิดใช้งาน / ปิดใช้งานโดยใช้พา ธ สัมพัทธ์ในไฟล์ปรับแต่ง

ในการปฏิบัติที่บริสุทธิ์ของรหัส / การออกแบบคุณจะต้องกำหนดพฤติกรรม / การกระทำเฉพาะที่ต้องการให้ผู้ใช้ของคุณขยาย ระบุจุดเริ่มต้นทั่วไป / ชุดของฟังก์ชันที่จะถูกเขียนทับและกำหนดกลุ่มภายในการกระทำเหล่านี้ เมื่อดำเนินการเสร็จแล้วควรขยายแอปพลิเคชันของคุณให้ง่ายขึ้น

ตัวอย่างการใช้hooksซึ่งได้แรงบันดาลใจจาก MediaWiki (PHP แต่ภาษามีความสำคัญหรือไม่):

import hooks

# In your core code, on key points, you allow user to run actions:
def compute(...):
    try:
        hooks.runHook(hooks.registered.beforeCompute)
    except hooks.hookException:
        print('Error while executing plugin')

    # [compute main code] ...

    try:
        hooks.runHook(hooks.registered.afterCompute)
    except hooks.hookException:
        print('Error while executing plugin')

# The idea is to insert possibilities for users to extend the behavior 
# where it matters.
# If you need to, pass context parameters to runHook. Remember that
# runHook can be defined as a runHook(*args, **kwargs) function, not
# requiring you to define a common interface for *all* hooks. Quite flexible :)

# --------------------

# And in the plugin code:
# [...] plugin magic
def doStuff():
    # ....
# and register the functionalities in hooks

# doStuff will be called at the end of each core.compute() call
hooks.registered.afterCompute.append(doStuff)

อีกตัวอย่างหนึ่งที่ได้รับแรงบันดาลใจมาจาก Mercurial ในที่นี้ส่วนขยายจะเพิ่มเฉพาะคำสั่งลงในบรรทัดคำสั่ง hgซึ่งสามารถขยายได้

def doStuff(ui, repo, *args, **kwargs):
    # when called, a extension function always receives:
    # * an ui object (user interface, prints, warnings, etc)
    # * a repository object (main object from which most operations are doable)
    # * command-line arguments that were not used by the core program

    doMoreMagicStuff()
    obj = maybeCreateSomeObjects()

# each extension defines a commands dictionary in the main extension file
commands = { 'newcommand': doStuff }

สำหรับวิธีการทั้งสองคุณอาจต้องร่วมกันการเริ่มต้นและจบสำหรับส่วนขยายของคุณ คุณสามารถใช้อินเทอร์เฟซทั่วไปที่ส่วนขยายทั้งหมดของคุณจะต้องนำไปใช้ (เหมาะกับแนวทางที่สองดีกว่า Mercurial ใช้ reposetup (ui, repo) ที่เรียกว่าสำหรับส่วนขยายทั้งหมด) หรือใช้วิธีการแบบตะขอกับ hook.setup hook

แต่อีกครั้งหากคุณต้องการคำตอบที่มีประโยชน์มากขึ้นคุณจะต้อง จำกัด คำถามของคุณให้แคบลง)


11

เฟรมเวิร์กปลั๊กอินง่าย ๆ ของ Marty Allchinคือพื้นฐานที่ฉันใช้สำหรับความต้องการของฉันเอง ฉันแนะนำและลองดูฉันคิดว่าเป็นการเริ่มต้นที่ดีถ้าคุณต้องการบางสิ่งที่ง่ายและแฮ็คได้ง่าย คุณสามารถค้นหาก็ยังเป็น Django เกร็ดเล็กเกร็ดน้อย


ฉันพยายามทำอะไรแบบนั้นกับ pyduck เป็นพื้นฐาน
edomaur

เป็นสิ่งที่เฉพาะเจาะจงมากของ Django จากสิ่งที่ฉันสามารถบอก
Zoran Pavlovic

3
@ ZoranPavlovic: ไม่เลย Python มาตรฐานบางบรรทัดคุณไม่จำเป็นต้องใช้ Django
edomaur

11

ฉันเป็นนักชีววิทยาที่เกษียณแล้วซึ่งจัดการกับ micrograqphs ดิจิทัลและพบว่าตัวเองต้องเขียนแพ็คเกจการประมวลผลและการวิเคราะห์ภาพ (ไม่ใช่เทคนิคห้องสมุด) เพื่อใช้กับเครื่อง SGi ฉันเขียนโค้ดใน C และใช้ Tcl สำหรับภาษาสคริปต์ GUI เช่นนี้เคยใช้ Tk คำสั่งที่ปรากฏใน Tcl เป็นรูปแบบ "extensionName commandName arg0 arg1 ... param0 param1 ... " นั่นคือคำและตัวเลขที่คั่นด้วยช่องว่างอย่างง่าย เมื่อ Tcl เห็นสตริงย่อย "extensionName" การควบคุมถูกส่งผ่านไปยังแพ็คเกจ C ในทางกลับกันคำสั่งวิ่งผ่าน lexer / parser (ทำใน lex / yacc) จากนั้นเรียก C รูทีนตามความจำเป็น

คำสั่งในการใช้งานแพคเกจสามารถเรียกใช้ทีละผ่านทางหน้าต่างใน GUI แต่งานแบทช์ทำโดยการแก้ไขไฟล์ข้อความซึ่งเป็นสคริปต์ Tcl ที่ถูกต้อง; คุณจะเลือกเทมเพลตที่ใช้การทำงานระดับไฟล์ที่คุณต้องการแล้วแก้ไขสำเนาเพื่อให้มีชื่อไดเรกทอรีและชื่อไฟล์จริงรวมถึงคำสั่งแพ็คเกจ มันทำงานเหมือนจับใจ จนกระทั่ง ...

1) โลกเปลี่ยนไปใช้พีซีและ 2) สคริปต์มีความยาวกว่า 500 บรรทัดเมื่อความสามารถขององค์กรที่เริ่มต้นของ Tcl กลายเป็นความไม่สะดวกที่แท้จริง เวลาผ่านไป ...

ฉันออก Python ถูกประดิษฐ์ขึ้นมาและดูเหมือนว่าจะเป็นผู้สืบทอดที่สมบูรณ์แบบต่อ Tcl ตอนนี้ฉันไม่เคยทำพอร์ตเพราะฉันไม่เคยเผชิญหน้ากับความท้าทายในการคอมไพล์โปรแกรม C (ใหญ่มาก) บนพีซีขยาย Python ด้วยแพ็คเกจ C และทำ GUIs ใน Python / Gt? / Tk? /? ? อย่างไรก็ตามแนวคิดเก่า ๆ ของการมีสคริปต์แม่แบบที่แก้ไขได้นั้นยังคงใช้งานได้ นอกจากนี้ไม่ควรเป็นภาระที่มากเกินไปในการป้อนคำสั่งแพ็คเกจในรูปแบบ Python ดั้งเดิมเช่น:

packageName.command (arg0, arg1, ... , param0, param1, ... )

จุดเพิ่มเติมสองสามคำ parens และเครื่องหมายจุลภาค แต่จุดนั้นไม่ได้แสดง

ฉันจำได้ว่าเห็นบางคนทำ lex และ yacc ใน Python (ลอง: http://www.dabeaz.com/ply/ ) ดังนั้นหากยังต้องการพวกมันอยู่รอบ ๆ

ประเด็นของการท่องเที่ยวครั้งนี้คือดูเหมือนว่าสำหรับฉันแล้ว Python นั้นเป็นส่วนหน้า "เบา" ที่ต้องการซึ่งนักวิทยาศาสตร์สามารถใช้ได้ ฉันอยากรู้ว่าทำไมคุณคิดว่ามันไม่ใช่และฉันหมายความอย่างจริงจัง


เพิ่มในภายหลัง: แอปพลิเคชันgeditคาดว่าจะมีการเพิ่มปลั๊กอินและไซต์ของพวกเขามีคำอธิบายที่ชัดเจนที่สุดเกี่ยวกับขั้นตอนการปลั๊กอินง่ายๆที่ฉันพบในไม่กี่นาทีที่มองไปรอบ ๆ ลอง:

https://wiki.gnome.org/Apps/Gedit/PythonPluginHowToOld

ฉันยังคงต้องการที่จะเข้าใจคำถามของคุณดีขึ้น ฉันไม่ชัดเจนว่าคุณ 1) ต้องการให้นักวิทยาศาสตร์สามารถใช้แอปพลิเคชัน (Python) ของคุณได้หลายวิธีหรือ 2) ต้องการให้นักวิทยาศาสตร์เพิ่มความสามารถใหม่ให้แอปพลิเคชันของคุณ ตัวเลือก # 1 คือสถานการณ์ที่เราเผชิญกับรูปภาพและนั่นทำให้เราใช้สคริปต์ทั่วไปที่เราปรับเปลี่ยนเพื่อให้เหมาะกับความต้องการในขณะนั้น เป็นตัวเลือก # 2 หรือไม่ที่นำคุณไปสู่แนวคิดของปลั๊กอินหรือเป็นบางส่วนของแอปพลิเคชันของคุณที่ทำให้การออกคำสั่งไม่สามารถทำได้?


2
การซ่อมแซมการเชื่อมโยง rot: ปลั๊กอิน Gedit ตอนนี้ - wiki.gnome.org/Apps/Gedit/PythonPluginHowTo
ohhorob

1
นี่คือโพสต์ที่สวยงามเพราะมันแสดงให้เห็นอย่างชัดเจนและรัดกุมเพียงแค่โชคดีที่เรานักชีววิทยาสมัยใหม่ สำหรับเขา / เธอแล้วไพ ธ อนเป็นภาษาสคริปต์แบบโมดูลาร์ที่ใช้เพื่อให้นามธรรมแก่นักพัฒนาโมดูลเพื่อให้พวกเขาไม่จำเป็นต้องแยกวิเคราะห์รหัส C หลัก อย่างไรก็ตามทุกวันนี้นักชีววิทยาบางคนจะเรียนรู้ C แทนการทำทุกอย่างใน Python เราจะสรุปความซับซ้อนของโปรแกรมหลามหลักของเราได้อย่างไรเมื่อเขียนโมดูล? ใน 10 ปีต่อจากนี้อาจจะเขียนโปรแกรมใน Emoji และโมดูลจะเป็นไฟล์เสียงที่มีชุดของคำราม และบางทีก็ตกลง
JJ

10

เมื่อฉันค้นหา Python Decorators พบข้อมูลโค้ดที่เรียบง่าย แต่มีประโยชน์ อาจไม่เหมาะกับความต้องการของคุณ แต่เป็นแรงบันดาลใจอย่างมาก

Scipy Advanced Python # Plugin ระบบการลงทะเบียน

class TextProcessor(object):
    PLUGINS = []

    def process(self, text, plugins=()):
        if plugins is ():
            for plugin in self.PLUGINS:
                text = plugin().process(text)
        else:
            for plugin in plugins:
                text = plugin().process(text)
        return text

    @classmethod
    def plugin(cls, plugin):
        cls.PLUGINS.append(plugin)
        return plugin


@TextProcessor.plugin
class CleanMarkdownBolds(object):
    def process(self, text):
        return text.replace('**', '')

การใช้งาน:

processor = TextProcessor()
processed = processor.process(text="**foo bar**", plugins=(CleanMarkdownBolds, ))
processed = processor.process(text="**foo bar**")

1
หมายเหตุ: ในตัวอย่างนี้WordProcessor.pluginไม่กลับอะไร ( None) เพื่อนำเข้าชั้นเรียนในภายหลังการนำเข้าเพียงCleanMdashesExtension Noneถ้าเรียนปลั๊กอินที่มีประโยชน์ของตัวเองทำให้วิธีการเรียน.plugin return plugin
jkmacc

@jkmacc คุณพูดถูก ฉันแก้ไขตัวอย่างข้อมูล 13 วันหลังจากความคิดเห็นของคุณ ขอบคุณ.
guneysus

7

ฉันสนุกกับการอภิปรายที่ดีเกี่ยวกับสถาปัตยกรรมปลั๊กอินที่กำหนดโดย Dr Andre Roberge ที่ Pycon 2009 เขาให้ภาพรวมที่ดีเกี่ยวกับวิธีการใช้งานปลั๊กอินที่หลากหลายโดยเริ่มจากสิ่งที่เรียบง่ายจริงๆ

มันสามารถใช้ได้เป็นพอดคาสต์ (ส่วนที่สองต่อไปนี้คำอธิบายของลิงปะ) ที่มาพร้อมกับชุดของหกรายการบล็อก

ฉันแนะนำให้ฟังก่อนตัดสินใจ


4

ฉันมาที่นี่เพื่อค้นหาสถาปัตยกรรมปลั๊กอินขั้นต่ำและพบสิ่งต่าง ๆ มากมายที่ทุกคนดูเหมือนจะเกินความคิดของฉัน ดังนั้นฉันจึงได้ใช้ง่ายสุดหลามปลั๊กอิน หากต้องการใช้งานคุณสร้างหนึ่งหรือหลายไดเรกทอรีและปล่อย__init__.pyไฟล์พิเศษในแต่ละไฟล์ การนำเข้าไดเรกทอรีเหล่านั้นจะทำให้ไฟล์ Python อื่น ๆ ทั้งหมดถูกโหลดเป็น submodules และชื่อของมันจะอยู่ใน__all__รายการ จากนั้นขึ้นอยู่กับคุณในการตรวจสอบ / เริ่มต้น / ลงทะเบียนโมดูลเหล่านั้น มีตัวอย่างในไฟล์ README


4

setuptoolsจริง ๆ แล้วทำงานร่วมกับ "ไดเรกทอรีปลั๊กอิน" เป็นตัวอย่างต่อไปนี้นำมาจากเอกสารของโครงการ: http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins

ตัวอย่างการใช้งาน:

plugin_dirs = ['foo/plugins'] + sys.path
env = Environment(plugin_dirs)
distributions, errors = working_set.find_plugins(env)
map(working_set.add, distributions)  # add plugins+libs to sys.path
print("Couldn't load plugins due to: %s" % errors)

ในระยะยาวsetuptoolsเป็นตัวเลือกที่ปลอดภัยกว่าเนื่องจากสามารถโหลดปลั๊กอินได้โดยไม่ขัดแย้งหรือขาดข้อกำหนด

ข้อดีอีกประการคือปลั๊กอินสามารถขยายได้โดยใช้กลไกเดียวกันโดยไม่ต้องใช้แอปพลิเคชันดั้งเดิมที่ต้องดูแล


3

เป็นหนึ่งในวิธีการอื่นเพื่อระบบปลั๊กอินคุณสามารถตรวจสอบขยายโครงการฉัน

ตัวอย่างเช่นลองนิยามคลาสแบบง่ายและส่วนขยาย

# Define base class for extensions (mount point)
class MyCoolClass(Extensible):
    my_attr_1 = 25
    def my_method1(self, arg1):
        print('Hello, %s' % arg1)

# Define extension, which implements some aditional logic
# or modifies existing logic of base class (MyCoolClass)
# Also any extension class maby be placed in any module You like,
# It just needs to be imported at start of app
class MyCoolClassExtension1(MyCoolClass):
    def my_method1(self, arg1):
        super(MyCoolClassExtension1, self).my_method1(arg1.upper())

    def my_method2(self, arg1):
        print("Good by, %s" % arg1)

และลองใช้ดู

>>> my_cool_obj = MyCoolClass()
>>> print(my_cool_obj.my_attr_1)
25
>>> my_cool_obj.my_method1('World')
Hello, WORLD
>>> my_cool_obj.my_method2('World')
Good by, World

และแสดงสิ่งที่ซ่อนอยู่หลังฉาก:

>>> my_cool_obj.__class__.__bases__
[MyCoolClassExtension1, MyCoolClass]

extend_meห้องสมุดกระบวนการสร้างปรุงแต่งระดับผ่าน metaclasses ดังนั้นในตัวอย่างข้างต้นเมื่อมีการสร้างตัวอย่างใหม่ของMyCoolClassเราได้เช่นการเรียนใหม่ที่เป็น subclass ของทั้งสองMyCoolClassExtensionและMyCoolClassมีการทำงานของทั้งสองคนต้องขอบคุณ ธมรดกหลาย

เพื่อควบคุมการสร้างคลาสได้ดีขึ้นมี metaclasses สองสามตัวที่กำหนดไว้ใน lib นี้:

  • ExtensibleType - ช่วยให้ขยายได้ง่ายโดย subclassing

  • ExtensibleByHashType - คล้ายกับ ExtensibleType แต่มีความสามารถในการสร้างคลาสเฉพาะของคลาสช่วยให้สามารถขยายคลาสพื้นฐานและส่วนขยายของคลาสเฉพาะของคลาสได้

lib นี้ใช้ในโครงการ OpenERP Proxyและดูเหมือนว่าจะทำงานได้ดีพอ!

สำหรับตัวอย่างการใช้งานจริงให้ดูในส่วนขยาย 'field_datetime' ของ OpenERP Proxy :

from ..orm.record import Record
import datetime

class RecordDateTime(Record):
    """ Provides auto conversion of datetime fields from
        string got from server to comparable datetime objects
    """

    def _get_field(self, ftype, name):
        res = super(RecordDateTime, self)._get_field(ftype, name)
        if res and ftype == 'date':
            return datetime.datetime.strptime(res, '%Y-%m-%d').date()
        elif res and ftype == 'datetime':
            return datetime.datetime.strptime(res, '%Y-%m-%d %H:%M:%S')
        return res

Recordนี่คือวัตถุที่ขยายออกได้ RecordDateTimeเป็นส่วนขยาย

หากต้องการเปิดใช้งานส่วนขยายเพียงแค่นำเข้าโมดูลที่มีคลาสส่วนขยายและ (ในกรณีข้างต้น) Recordวัตถุทั้งหมดที่สร้างขึ้นหลังจากนั้นจะมีคลาสส่วนขยายในคลาสฐานจึงมีการใช้งานทั้งหมด

ข้อได้เปรียบหลักของไลบรารีนี้คือรหัสที่ใช้งานวัตถุที่ขยายได้ไม่จำเป็นต้องรู้เกี่ยวกับส่วนขยายและส่วนขยายสามารถเปลี่ยนแปลงทุกอย่างในวัตถุที่ขยายได้


ฉันคิดว่าคุณหมายถึงการยกตัวอย่างจากคลาสย่อยเช่นmy_cool_obj = MyCoolClassExtension1()แทนที่จะเป็นmy_cool_obj = MyCoolClass()
pylang

ไม่คลาส Extensible มี__new__วิธีการเขียนทับดังนั้นมันจะค้นหาคลาสย่อยทั้งหมดโดยอัตโนมัติและสร้างคลาสใหม่นั่นคือคลาสย่อยของพวกเขาทั้งหมดและส่งคืนอินสแตนซ์ใหม่ของคลาสที่สร้างขึ้นนี้ ดังนั้นแอปพลิเคชันดั้งเดิมไม่จำเป็นต้องรู้เกี่ยวกับส่วนขยายทั้งหมด วิธีการนี้มีประโยชน์เมื่อสร้างห้องสมุดเพื่อให้ผู้ใช้สามารถปรับเปลี่ยนหรือขยายได้อย่างง่ายดาย ในตัวอย่างข้างต้น MyCoolClass อาจถูกกำหนดในไลบรารี่และใช้โดยมันและ MyCoolClassExtension สามารถกำหนดโดยผู้ใช้ปลายทางได้
FireMage

อีกตัวอย่างหนึ่งถูกเพิ่มเข้ามาเพื่อตอบ
FireMage

3

setuptools มีจุดเข้าใช้งาน :

จุดเริ่มต้นเป็นวิธีที่ง่ายสำหรับการแจกจ่ายเพื่อ "โฆษณา" วัตถุ Python (เช่นฟังก์ชั่นหรือคลาส) สำหรับการใช้งานโดยการกระจายอื่น ๆ แอปพลิเคชันและเฟรมเวิร์กที่ขยายได้สามารถค้นหาจุดเข้าใช้งานที่มีชื่อหรือกลุ่มเฉพาะจากการกระจายเฉพาะหรือจากการแจกแจงที่แอ็คทีฟทั้งหมดบน sys.path จากนั้นตรวจสอบหรือโหลดออบเจ็กต์โฆษณาตามต้องการ

AFAIK แพ็คเกจนี้ใช้ได้เสมอหากคุณใช้ pip หรือ virtualenv


2

ขยายตัวในคำตอบของ @ edomaur อาจผมขอแนะนำการดูที่simple_plugins (ไร้ยางอายปลั๊ก) ซึ่งเป็นกรอบปลั๊กอินง่ายแรงบันดาลใจจากผลงานของมาร์ตี้ Alchin

ตัวอย่างการใช้งานสั้น ๆ ตาม README ของโครงการ:

# All plugin info
>>> BaseHttpResponse.plugins.keys()
['valid_ids', 'instances_sorted_by_id', 'id_to_class', 'instances',
 'classes', 'class_to_id', 'id_to_instance']

# Plugin info can be accessed using either dict...
>>> BaseHttpResponse.plugins['valid_ids']
set([304, 400, 404, 200, 301])

# ... or object notation
>>> BaseHttpResponse.plugins.valid_ids
set([304, 400, 404, 200, 301])

>>> BaseHttpResponse.plugins.classes
set([<class '__main__.NotFound'>, <class '__main__.OK'>,
     <class '__main__.NotModified'>, <class '__main__.BadRequest'>,
     <class '__main__.MovedPermanently'>])

>>> BaseHttpResponse.plugins.id_to_class[200]
<class '__main__.OK'>

>>> BaseHttpResponse.plugins.id_to_instance[200]
<OK: 200>

>>> BaseHttpResponse.plugins.instances_sorted_by_id
[<OK: 200>, <MovedPermanently: 301>, <NotModified: 304>, <BadRequest: 400>, <NotFound: 404>]

# Coerce the passed value into the right instance
>>> BaseHttpResponse.coerce(200)
<OK: 200>

2

ฉันใช้เวลาอ่านหัวข้อนี้ในขณะที่ฉันค้นหาเฟรมเวิร์กปลั๊กอินใน Python ตั้งแต่นี้มา ฉันเคยใช้บางส่วน แต่มีข้อบกพร่องกับพวกเขา นี่คือสิ่งที่ฉันมากับเพื่อตรวจสอบข้อเท็จจริงของคุณในปี 2017 อินเตอร์เฟซฟรีคู่หลวมปลั๊กอินระบบการจัดการ: โหลดฉันในภายหลัง นี่คือบทเรียนเกี่ยวกับวิธีการใช้งาน


2

คุณสามารถใช้pluginlib

ปลั๊กอินนั้นสร้างได้ง่ายและสามารถโหลดได้จากแพ็คเกจไฟล์พา ธ หรือจุดเข้าใช้งานอื่น ๆ

สร้างคลาสพาเรนต์ของปลั๊กอินโดยกำหนดวิธีที่ต้องการ:

import pluginlib

@pluginlib.Parent('parser')
class Parser(object):

    @pluginlib.abstractmethod
    def parse(self, string):
        pass

สร้างปลั๊กอินโดยการสืบทอดคลาสพาเรนต์:

import json

class JSON(Parser):
    _alias_ = 'json'

    def parse(self, string):
        return json.loads(string)

โหลดปลั๊กอิน:

loader = pluginlib.PluginLoader(modules=['sample_plugins'])
plugins = loader.plugins
parser = plugins.parser.json()
print(parser.parse('{"json": "test"}'))

1
ขอบคุณสำหรับตัวอย่าง ฉันกำลังดิ้นรนกับคำถาม 1 ข้อ เนื่องจากคุณพูดถึงความเป็นไปได้ในการโหลดปลั๊กอินจากแพ็คเกจต่าง ๆ บางทีคุณอาจคิดอยู่แล้ว ฉันสงสัยว่าผู้ปกครองควรอาศัยอยู่ที่ใด โดยปกติจะแนะนำให้มีในแพ็คเกจของแอปพลิเคชัน (น่าจะเป็นที่เก็บซอร์สโค้ดที่แยกต่างหาก) แต่แล้วเราจะสืบทอดจากมันได้อย่างไรที่ codebase ของปลั๊กอิน เราจะนำเข้าแอปพลิเคชันทั้งหมดสำหรับสิ่งนี้หรือไม่ หรือจำเป็นต้องมีรหัสอินเตอร์เฟสเช่นคลาส Parser หรือ abstractions ที่คล้ายกันในแพ็คเกจที่ 3 (ซึ่งจะเป็นที่เก็บรหัสที่ 3)
JAponte

1
คลาสพาเรนต์ควรอยู่ในฐานรหัสเดียวกันกับแอปพลิเคชัน แต่อาจอยู่ในโมดูลของตัวเอง ดังนั้นสำหรับแพ็คเกจที่เรียกว่าfooคุณอาจมีโมดูลที่เรียกว่าfoo.parentsตำแหน่งที่คุณกำหนดคลาสพาเรนต์ foo.parentsจากนั้นปลั๊กอินของคุณจะนำเข้า ใช้งานได้ดีกับกรณีส่วนใหญ่ เนื่องจากตัวเอง 'foo' ยังได้รับการนำเข้าเพื่อหลีกเลี่ยงความเป็นไปได้ของการนำเข้าแบบวงกลมโครงการจำนวนมากปล่อยให้รูทของโมดูลว่างเปล่าและใช้__main__.pyไฟล์หรือจุดเข้าเพื่อเรียกใช้แอปพลิเคชัน
aviso

1

ฉันใช้เวลาพยายามหาระบบปลั๊กอินขนาดเล็กสำหรับ Python ซึ่งจะเหมาะกับความต้องการของฉัน แต่ฉันก็คิดว่าถ้ามีการถ่ายทอดทางพันธุกรรมซึ่งเป็นธรรมชาติและมีความยืดหยุ่นอยู่แล้วทำไมไม่ใช้มัน

ปัญหาเดียวของการใช้การสืบทอดสำหรับปลั๊กอินคือคุณไม่รู้ว่าคลาสปลั๊กอินที่เฉพาะเจาะจงมากที่สุด (น้อยที่สุดในแผนผังการสืบทอด) คืออะไร

แต่สิ่งนี้สามารถแก้ไขได้ด้วย metaclass ซึ่งติดตามการสืบทอดของคลาสฐานและอาจสร้างคลาสได้ซึ่งสืบทอดมาจากปลั๊กอินเฉพาะส่วนใหญ่ ('รูตขยาย' ในรูปด้านล่าง)

ป้อนคำอธิบายรูปภาพที่นี่

ดังนั้นฉันจึงมาพร้อมทางออกโดยการเข้ารหัส metaclass ดังกล่าว:

class PluginBaseMeta(type):
    def __new__(mcls, name, bases, namespace):
        cls = super(PluginBaseMeta, mcls).__new__(mcls, name, bases, namespace)
        if not hasattr(cls, '__pluginextensions__'):  # parent class
            cls.__pluginextensions__ = {cls}  # set reflects lowest plugins
            cls.__pluginroot__ = cls
            cls.__pluginiscachevalid__ = False
        else:  # subclass
            assert not set(namespace) & {'__pluginextensions__',
                                         '__pluginroot__'}     # only in parent
            exts = cls.__pluginextensions__
            exts.difference_update(set(bases))  # remove parents
            exts.add(cls)  # and add current
            cls.__pluginroot__.__pluginiscachevalid__ = False
        return cls

    @property
    def PluginExtended(cls):
        # After PluginExtended creation we'll have only 1 item in set
        # so this is used for caching, mainly not to create same PluginExtended
        if cls.__pluginroot__.__pluginiscachevalid__:
            return next(iter(cls.__pluginextensions__))  # only 1 item in set
        else:
            name = cls.__pluginroot__.__name__ + 'PluginExtended'
            extended = type(name, tuple(cls.__pluginextensions__), {})
            cls.__pluginroot__.__pluginiscachevalid__ = True
return extended

ดังนั้นเมื่อคุณมีฐานรูทซึ่งทำด้วยเมตาคลาสและมีแผนผังของปลั๊กอินที่สืบทอดมาคุณจะได้คลาสโดยอัตโนมัติซึ่งสืบทอดมาจากปลั๊กอินที่เฉพาะเจาะจงมากที่สุดโดยเพียงแค่คลาสย่อย:

class RootExtended(RootBase.PluginExtended):
    ... your code here ...

รหัสฐานมีขนาดค่อนข้างเล็ก (ประมาณ 30 บรรทัดของรหัสบริสุทธิ์) และมีความยืดหยุ่นเท่ากับการรับมรดก

หากคุณสนใจเข้าร่วม @ https://github.com/thodnev/pluginlib


1

คุณอาจจะดูที่รากฐานรากฐาน

แนวคิดคือการสร้างแอปพลิเคชันรอบส่วนประกอบที่ใช้ซ้ำได้ซึ่งเรียกว่ารูปแบบและปลั๊กอิน GwBasePatternปลั๊กอินชั้นเรียนที่ได้รับจาก นี่คือตัวอย่างพื้นฐาน:

from groundwork import App
from groundwork.patterns import GwBasePattern

class MyPlugin(GwBasePattern):
    def __init__(self, app, **kwargs):
        self.name = "My Plugin"
        super().__init__(app, **kwargs)

    def activate(self): 
        pass

    def deactivate(self):
        pass

my_app = App(plugins=[MyPlugin])       # register plugin
my_app.plugins.activate(["My Plugin"]) # activate it

นอกจากนี้ยังมีรูปแบบขั้นสูงเพิ่มเติมเพื่อจัดการเช่นอินเตอร์เฟสบรรทัดคำสั่งการส่งสัญญาณหรือวัตถุที่ใช้ร่วมกัน

Groundwork ค้นหาปลั๊กอินโดยการผูกโปรแกรมเข้ากับแอปตามที่แสดงไว้ด้านบนหรือโดยอัตโนมัติผ่านทางsetuptoolsโปรแกรม แพ็คเกจ Python ที่มีปลั๊กอินต้องประกาศสิ่งเหล่านี้โดยใช้จุดเข้าใช้พิเศษgroundwork.pluginแพคเกจหลามที่มีปลั๊กอินต้องประกาศเหล่านี้โดยใช้จุดเข้าพิเศษ

นี่เป็นเอกสาร

คำเตือน : ฉันเป็นหนึ่งในผู้เขียนของ Groundwork


0

ในผลิตภัณฑ์ดูแลสุขภาพของเราในปัจจุบันเรามีสถาปัตยกรรมปลั๊กอินที่ใช้กับคลาสอินเตอร์เฟส กองเทคโนโลยีของเราคือ Django บน Python สำหรับ API และ Nuxtjs ที่ด้านบนของ nodejs สำหรับส่วนหน้า

เรามีแอปตัวจัดการปลั๊กอินที่เขียนขึ้นสำหรับผลิตภัณฑ์ของเราซึ่งโดยพื้นฐานแล้วเป็นแพคเกจ pip และ npm โดยยึดมั่นกับ Django และ Nuxtjs

สำหรับการพัฒนาปลั๊กอินใหม่ (pip และ npm) เราทำให้ตัวจัดการปลั๊กอินเป็นการพึ่งพา

ใน Pip package: ด้วยความช่วยเหลือของ setup.py คุณสามารถเพิ่มจุดเข้าใช้งานของปลั๊กอินเพื่อทำบางสิ่งด้วยตัวจัดการปลั๊กอิน (การลงทะเบียน, การเริ่มต้น, ... ฯลฯ ) https://setuptools.readthedocs.io/en/latest/setuptools .html # อัตโนมัติสคริปต์การสร้าง

ในแพ็คเกจ npm: คล้ายกับ pip มี hooks ในสคริปต์ npm เพื่อจัดการกับการติดตั้ง https://docs.npmjs.com/misc/scripts

usecase ของเรา:

ทีมพัฒนาปลั๊กอินแยกออกจากทีมพัฒนาแกนหลักทันที ขอบเขตของการพัฒนาปลั๊กอินสำหรับการรวมเข้ากับแอพของบุคคลที่สามซึ่งกำหนดไว้ในหมวดหมู่ใด ๆ ของผลิตภัณฑ์ อินเทอร์เฟซปลั๊กอินถูกจัดหมวดหมู่สำหรับเช่น: - แฟกซ์, โทรศัพท์, อีเมล์ ... ฯลฯ ตัวจัดการปลั๊กอินสามารถปรับปรุงเป็นหมวดหมู่ใหม่ได้

ในกรณีของคุณ: บางทีคุณอาจมีปลั๊กอินหนึ่งตัวที่เขียนและนำมาใช้ซ้ำสำหรับการทำสิ่งของ

หากนักพัฒนาปลั๊กอินจำเป็นต้องใช้วัตถุหลักที่สามารถนำมาใช้ใหม่ได้โดยทำในระดับที่เป็นนามธรรมภายในตัวจัดการปลั๊กอินเพื่อให้ปลั๊กอินใด ๆ สามารถสืบทอดวิธีการเหล่านั้นได้

เพียงแบ่งปันวิธีที่เรานำไปใช้ในผลิตภัณฑ์ของเราหวังว่ามันจะให้ความคิดเล็ก ๆ น้อย ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.