是否可以将类方法定义为闪亮的Python模块?

问题描述 投票:0回答:1

我正在尝试构建一个 Shiny for Python 应用程序,其中可以使用 Shiny 模块分解部分表单代码,在其中我可以将特定的 ui 和服务器逻辑定义为类方法,但最终继承一些基本功能。 我有以下闪亮的应用程序:

import pandas as pd
from shiny import App, reactive, ui, module, render
from abc import ABC, abstractmethod


class ShinyFormTemplate(ABC):
    @abstractmethod
    @module.ui
    def ui_func(self):
        pass

    @abstractmethod
    @module.server
    def server_func(self, input, output, session, *args, **kwargs):
        pass


class FruitForm(ShinyFormTemplate):
    @module.ui
    def ui_func(self):
        return ui.row(
            ui.input_text("fruits",'Select a Fruit',""),
            ui.output_text("output_fruit"),
            ui.input_text("qtys",'Quantity:',""),
            ui.output_text("output_qty"),
        )

    @module.server
    def server_func(self, input, output, session, *args, **kwargs):
        @render.text
        def output_fruits():
            return input.fruits()
        
        @render.text
        def output_qty():
            return input.qtys()

class VeggieForm(ShinyFormTemplate):
    @module.ui
    def ui_func(self):
        return ui.row(
            ui.input_radio_buttons("veggie","Select a Veggie:",{'Asparagus':'Asparagus','Spinach':'Spinach','Squash':'Squash','Lettuce':'Lettuce'}),
            ui.output_text("output_veggie"),
            ui.input_text("qtys",'Quantity:',""),
            ui.output_text("output_qty"),
        )

    @module.server
    def server_func(self, input, output, session, *args, **kwargs):
        @render.text
        def output_veggie():
            return input.veggie()
        
        @render.text
        def output_qty():
            return input.qtys()

fruits = FruitForm()
veggies = VeggieForm()



app_ui = ui.page_fluid(
    ui.page_navbar(
        ui.nav_panel("New Fruit", "New Fruit - Input Form",
            fruits.ui_func('fruit')
        ),

        ui.nav_panel("New Veggie", "New Veggie - Input Form",
            veggies.ui_func('veggie')
        ),
        title="Sample App",
        id="page",
    ),
    title="Basic App"
)

def server(input, output, session):
    fruits.server_func('fruit')
    veggies.server_func('veggie')


app = App(app_ui, server)

但是当尝试运行它时,我收到错误:

Exception has occurred: ValueError
`id` must be a single string
  File "C:\Users\this_user\OneDrive\Documents\Programming Projects\Python\example_class_module\app.py", line 66, in <module>
    fruits.ui_func('fruit')
ValueError: `id` must be a single string

我不明白这个错误,因为我以为我为模块命名空间提供了一个基于字符串的 id(在本例中为“fruit”)。

我尝试过的: 我尝试在类外部定义服务器和 ui 对象,并将它们作为参数传递给 init() 函数,而不是将它们定义为类方法。 但是,以这种方式工作,我认为我无法使用 self.attribute 从模块函数内访问类实例的属性。

是否可以将类方法定义为闪亮的模块(服务器和用户界面)组件?

python shinymodules py-shiny
1个回答
0
投票

我找到了一种方法可以完成所有想要的事情,但我不确定这是最好的方法。 如果有更好的方法欢迎建议..

无需将 ui 或服务器模块显式定义为类方法,如果在类方法内定义 ui 和服务器模块,然后在方法末尾调用以激活模块代码,则可以实现类似的功能。 这是基本想法:

class MyForm:
   def __init__(self, namespace_id): # pass in other objects specific to this form, such as a dataframe to populate the form with initially
      self.__namespace_id = namespace_id

   def call_ui(self):
      @module.ui
      def ui_func():
         # ui module components go here for the module

      return ui_func(self.__namespace_id)

   def call_server(self):
      @module.server
      def server_func(self, input, output, session):
         # server module logic goes here

      server_func(self.__namespace_id)

form = MyForm('fruit') # the module in this class uses the 'fruit' namespace

app_ui = ui.page_fluid(
   form.call_ui()
)

def server(input, output, session):
   form.call_server(input, output, session)
   # input, output, and session must be explicitly passed into call_server method

app = App(app_ui, server)

这最终使得以模块化方式构建更复杂的应用程序成为可能,例如,该应用程序可以拥有一堆 UI 表单,捕获自己的特定输入字段并写入自己的特定数据库表。

下面是一个根据问题构建的工作示例,演示了类中闪亮模块的封装、父类(ShinyFormTemplate)中定义的公共模块功能的继承,以及 call_server() 和 call_ui() 方法的多态行为处理特定于其子类的表单内容(FruitForm* 和 VeggieForm)。 另请注意,VeggieForm 类在其 _init() 方法中定义了额外的复杂性,以将其与 FruitForm 区分开来(请参阅仅 FruitForm 需要的 veggie_only_data 参数)。

import pandas as pd
from shiny import App, reactive, ui, module, render
from abc import ABC, abstractmethod


class ShinyFormTemplate(ABC):
    """This is the abstract base class that has some commonly defined and implemented ui and server logic methods, as well as abstract methods for ui and server logic methods that will be implemented by the children (FruitForm and VeggieForm)"""

    _namespace_id=None

    def __init__(self, namespace_id, *args, **kwargs): # will be inhereted by child classes
        self._namespace_id=namespace_id

    @abstractmethod
    def call_ui(self):
        pass

    @abstractmethod
    def call_server(self,input,output,session):
        pass

    def _common_button_ui(self):
        """This method gets inherited by both fruit and veggie classes, providing ui for counter button"""
        return ui.row(

            ui.column(6,ui.input_action_button('count_button',"Increment Counter")),
            ui.column(3,ui.output_text('show_counter')),
            ui.column(3),
        )
    
    def _common_button_server(self,input,output,session):
        """This method gets inherited by both fruit and veggie classes, providing server functionality for counter button"""
        counter = reactive.value(0)

        @reactive.effect
        @reactive.event(input.count_button)
        def namespace_text():
            counter.set(counter.get()+1)

        @render.text
        def show_counter():
            return str(counter())

class FruitForm(ShinyFormTemplate):
    """This is the Fruit child class providing specific UI and Server functionality for Fruits."""
    def call_ui(self):
        """This method defines the FruitForm specific ui module AND calls it at the end returning the result."""
        @module.ui
        def ui_func():
            return ui.row(
            ui.input_text("fruits",'Select a Fruit',""),
            ui.output_text("output_fruits"),
            ui.input_text("qtys",'Quantity:',""),
            ui.output_text("output_qty"),
            self._common_button_ui(), # protected method inherited from ShinyFormTemplate.  

            # Insert additional fruit specific ui here that will operate in the 'fruit' namespace
        )
        return ui_func(self._namespace_id) # the call to establish the fruit ui has to be returned at the end of this class, so that it gets inserted into the app_ui object that is defined globally

    def call_server(self,input,output,session):
        """This method defines the ui module AND calls it at the end."""
        @module.server
        def server_func(input, output, session):
            @render.text
            def output_fruits():
                return input.fruits()
            
            self.__server_fruit_addl_stuff(input, output, session) # private method for FruitForm class only
            # Insert additional Fruit specific server logic here that will operate in the 'fruit' namespace
            self._common_button_server(input,output,session) # protected method inherited from ShinyFormTemplate
            
        server_func(self._namespace_id)

    def __server_fruit_addl_stuff(self, input, output, session):
            """Here is some additional server functionality that exists only in the FruitForm class and can be called by the call_server() method"""
            @render.text
            def output_qty():
                return input.qtys()

class VeggieForm(ShinyFormTemplate):
    """This is the Veggie child class providing specific UI and Server functionality for Veggies."""

    def __init__(self, namespace_id, veggie_only_data, *args, **kwargs): # will be inhereted by child classes
        self._namespace_id=namespace_id    
        self.__veggie_only_data=veggie_only_data
    
    def call_ui(self):
        """This method defines the VeggieForm specific ui module AND calls it at the end returning the result."""
        @module.ui
        def ui_func():
            return ui.row(
                ui.row(self.__veggie_only_data).add_style("font-weight: bold;"),
                ui.input_radio_buttons("veggie","Select a Veggie:",{'Asparagus':'Asparagus','Spinach':'Spinach','Squash':'Squash','Lettuce':'Lettuce'}),
                ui.output_text("output_veggie"),
                ui.input_text("qtys",'Quantity:',""),
                ui.output_text("output_qty"),
                # Insert additional Veggie specific ui here that will operate in the 'veggie' namespace
                self._common_button_ui(),
        )
        return ui_func(self._namespace_id)


    def call_server(self,input,output,session):
        @module.server
        def server_func(input, output, session):
            @render.text
            def output_veggie():
                return input.veggie()
        
            @render.text
            def output_qty():
                return input.qtys()
            
            # Insert additional Veggie specific server logic here that will operate in the 'veggie' namespace
            self._common_button_server(input, output, session)
        
        server_func(self._namespace_id)

#Define fruit and veggie class object instances.  This allows us to also pass in other objects like specific dataframes and database models that we can use to write populated form data to PostgreSQL database
fruits = FruitForm(namespace_id='fruit') # all ui/server components will operate in the 'fruit' namespace
veggies = VeggieForm(namespace_id='veggie',veggie_only_data='This class has a veggie specific data model') # all ui/server components will operate in the 'fruit' namespace

# main ui object for the app
app_ui = ui.page_fluid(
    ui.page_navbar(
        ui.nav_panel("New Fruit", "New Fruit - Input Form",
            fruits.call_ui() # calling this will insert all the fruit specific UI
        ),

        ui.nav_panel("New Veggie", "New Veggie - Input Form",
            veggies.call_ui() # calling this will insert all the veggie specific UI
        ),
        title="Sample App",
        id="page",
    ),
    title="Basic App"
)

# main server function for the app
def server(input, output, session):
    fruits.call_server(input, output, session) # creates the server module - must pass input, output, and session in
    veggies.call_server(input, output, session) # creates the server module - must pass input, output, and session in


app = App(app_ui, server)

Example Modular Form with Classes

© www.soinside.com 2019 - 2024. All rights reserved.