This is an automated email from the ASF dual-hosted git repository.

shuaijinchao pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/apisix-python-plugin-runner.git


The following commit(s) were added to refs/heads/master by this push:
     new 342630e  refactor(plugin): plugins automatically register and standard 
interfaces (#41)
342630e is described below

commit 342630ed2e29c9aa81cf31e299e15ab479a8e6d2
Author: 帅进超 <[email protected]>
AuthorDate: Mon Feb 14 09:23:59 2022 +0800

    refactor(plugin): plugins automatically register and standard interfaces 
(#41)
---
 Makefile                         |   2 +-
 apisix/plugins/rewrite.py        |  32 ++++++---
 apisix/plugins/stop.py           |  32 ++++++---
 apisix/runner/http/request.py    |  16 ++---
 apisix/runner/plugin/base.py     |  64 ------------------
 apisix/runner/plugin/core.py     | 104 ++++++++++++++++++++---------
 apisix/runner/server/handle.py   |   2 +-
 bin/py-runner                    |  10 +--
 tests/runner/plugin/test_base.py |  38 -----------
 tests/runner/plugin/test_core.py | 138 ++++++++++++++++++++++++++-------------
 10 files changed, 223 insertions(+), 215 deletions(-)

diff --git a/Makefile b/Makefile
index f60fadf..6f28d41 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ lint: clean
 
 .PHONY: clean
 clean:
-       rm -rf apache_apisix.egg-info dist build .coverage
+       rm -rf apache_apisix.egg-info dist build assets .coverage report.html
        find . -name "__pycache__" -exec rm -r {} +
        find . -name ".pytest_cache" -exec rm -r {} +
        find . -name "*.pyc" -exec rm -r {} +
diff --git a/apisix/plugins/rewrite.py b/apisix/plugins/rewrite.py
index 12543dc..e44900f 100644
--- a/apisix/plugins/rewrite.py
+++ b/apisix/plugins/rewrite.py
@@ -15,31 +15,43 @@
 # limitations under the License.
 #
 
-from apisix.runner.plugin.base import Base
+from typing import Any
 from apisix.runner.http.request import Request
 from apisix.runner.http.response import Response
+from apisix.runner.plugin.core import PluginBase
 
 
-class Rewrite(Base):
-    def __init__(self):
+class Rewrite(PluginBase):
+
+    def name(self) -> str:
         """
-        Examples of `rewrite` type plugins, features:
-            This type of plugin can customize the request `args`, `header`, 
and `path`
-            This type of plugin does not interrupt the request
+        The name of the plugin registered in the runner
+        :return:
         """
-        super(Rewrite, self).__init__(self.__class__.__name__)
+        return "rewrite"
 
-    def filter(self, request: Request, response: Response):
+    def config(self, conf: Any) -> Any:
+        """
+        Parse plugin configuration
+        :param conf:
+        :return:
+        """
+        return conf
+
+    def filter(self, conf: Any, request: Request, response: Response):
         """
         The plugin executes the main function
+        :param conf:
+            plugin configuration after parsing
         :param request:
             request parameters and information
         :param response:
             response parameters and information
         :return:
         """
-        # Get plugin configuration information through `self.config`
-        # print(self.config)
+
+        # print plugin configuration
+        # print(conf)
 
         # Rewrite request headers
         request.headers["X-Resp-A6-Runner"] = "Python"
diff --git a/apisix/plugins/stop.py b/apisix/plugins/stop.py
index f0bdd8f..86ce5e9 100644
--- a/apisix/plugins/stop.py
+++ b/apisix/plugins/stop.py
@@ -15,31 +15,43 @@
 # limitations under the License.
 #
 
-from apisix.runner.plugin.base import Base
+from typing import Any
 from apisix.runner.http.request import Request
 from apisix.runner.http.response import Response
+from apisix.runner.plugin.core import PluginBase
 
 
-class Stop(Base):
-    def __init__(self):
+class Stop(PluginBase):
+
+    def name(self) -> str:
         """
-        Example of `stop` type plugin, features:
-            This type of plugin can customize response `body`, `header`, 
`http_code`
-            This type of plugin will interrupt the request
+        The name of the plugin registered in the runner
+        :return:
         """
-        super(Stop, self).__init__(self.__class__.__name__)
+        return "stop"
 
-    def filter(self, request: Request, response: Response):
+    def config(self, conf: Any) -> Any:
+        """
+        Parse plugin configuration
+        :param conf:
+        :return:
+        """
+        return conf
+
+    def filter(self, conf: Any, request: Request, response: Response):
         """
         The plugin executes the main function
+        :param conf:
+            plugin configuration after parsing
         :param request:
             request parameters and information
         :param response:
             response parameters and information
         :return:
         """
-        # Get plugin configuration information through `self.config`
-        # print(self.config)
+
+        # print plugin configuration
+        # print(conf)
 
         # Set response headers
         response.headers["X-Resp-A6-Runner"] = "Python"
diff --git a/apisix/runner/http/request.py b/apisix/runner/http/request.py
index 1764839..a318198 100644
--- a/apisix/runner/http/request.py
+++ b/apisix/runner/http/request.py
@@ -15,9 +15,7 @@
 # limitations under the License.
 #
 
-import json
 import flatbuffers
-import apisix.runner.plugin.core as runner_plugin
 import apisix.runner.utils.common as runner_utils
 
 from ipaddress import IPv4Address
@@ -290,18 +288,12 @@ class Request:
             if req.ConfIsNone():
                 return
 
-            # loading plugin
-            plugins = runner_plugin.loading()
             configs = {}
             for i in range(req.ConfLength()):
-                name = str(req.Conf(i).Name().decode()).lower()
-                plugin = plugins.get(name)
-                if not plugin:
-                    continue
-                value = req.Conf(i).Value().decode()
-                plugin = plugin()
-                plugin.config = json.loads(value)
-                configs[name] = plugin
+                # fetch request config
+                name = req.Conf(i).Name().decode()
+                config = req.Conf(i).Value().decode()
+                configs[name] = config
             self.configs = configs
 
     def checked(self):
diff --git a/apisix/runner/plugin/base.py b/apisix/runner/plugin/base.py
deleted file mode 100644
index 6a2202d..0000000
--- a/apisix/runner/plugin/base.py
+++ /dev/null
@@ -1,64 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-
-class Base:
-    def __init__(self, name: str):
-        """
-        plugin base class
-        :param name:
-            instance plugin name
-        """
-        self._name = name
-        self._config = {}
-
-    @property
-    def name(self) -> str:
-        """
-        get plugin type
-        :return:
-        """
-        return self._name
-
-    @name.setter
-    def name(self, name: str) -> None:
-        """
-        set plugin type
-        :param name:
-        :return:
-        """
-        self._name = name
-
-    @property
-    def config(self) -> dict:
-        """
-        set plugin config
-        :return:
-        """
-        return self._config
-
-    @config.setter
-    def config(self, config: dict) -> None:
-        """
-        get plugin config
-        :param config:
-        :return:
-        """
-        if config and isinstance(config, dict):
-            self._config = config
-        else:
-            self._config = {}
diff --git a/apisix/runner/plugin/core.py b/apisix/runner/plugin/core.py
index f130b97..af2e827 100644
--- a/apisix/runner/plugin/core.py
+++ b/apisix/runner/plugin/core.py
@@ -17,38 +17,82 @@
 
 import os
 import importlib
+from typing import Any
 from pkgutil import iter_modules
 from apisix.runner.http.response import Response as HttpResponse
 from apisix.runner.http.request import Request as HttpRequest
 
+PLUGINS = {}
 
-def execute(configs: dict, r, req: HttpRequest, reps: HttpResponse) -> bool:
-    for name in configs:
-        plugin = configs.get(name)
-        if type(plugin).__name__.lower() != name.lower():
-            r.log.error("execute plugin `%s`, plugin handler is not object" % 
name)
-            return False
-
-        try:
-            plugin.filter(req, reps)
-        except AttributeError as e:
-            r.log.error("execute plugin `%s` AttributeError, %s" % (name, 
e.args.__str__()))
-            return False
-        except TypeError as e:
-            r.log.error("execute plugin `%s` TypeError, %s" % (name, 
e.args.__str__()))
-            return False
-    return True
-
-
-def loading() -> dict:
-    path = "%s/plugins" % 
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
-    modules = iter_modules(path=[path])
-    plugins = {}
-
-    for loader, moduleName, _ in modules:
-        classNameConversion = list(map(lambda name: name.capitalize(), 
moduleName.split("_")))
-        className = "".join(classNameConversion)
-        classInstance = getattr(importlib.import_module("apisix.plugins.%s" % 
moduleName), className)
-        plugins[str(moduleName).lower()] = classInstance
-
-    return plugins
+
+class PluginBase:
+
+    def __init_subclass__(cls: Any, **kwargs):
+        """
+        register plugin object
+        :param kwargs:
+        :return:
+        """
+        name = cls.name(cls)
+        if name not in PLUGINS:
+            PLUGINS[name] = cls
+
+    def name(self) -> str:
+        """
+        fetching plugin name
+        :return:
+        """
+        pass
+
+    def config(self, conf: Any) -> Any:
+        """
+        parsing plugin configuration
+        :return:
+        """
+        pass
+
+    def filter(self, conf: Any, req: HttpRequest, reps: HttpResponse) -> None:
+        """
+        execute plugin handler
+        :param conf:  plugin configuration
+        :param req:   request object
+        :param reps:  response object
+        :return:
+        """
+        pass
+
+
+class PluginProcess:
+    """
+    plugin default package name
+    """
+    package = "apisix.plugins"
+
+    @staticmethod
+    def register():
+        plugin_path = "%s/%s" % 
(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
+                                 PluginProcess.package.replace(".", "/"))
+        modules = iter_modules(path=[plugin_path])
+        for _, mod_name, _ in modules:
+            importlib.import_module("%s.%s" % (PluginProcess.package, 
mod_name))
+
+    @staticmethod
+    def execute(configs: dict, r, req: HttpRequest, reps: HttpResponse):
+        for name, conf in configs.items():
+            try:
+                p = PLUGINS.get(name)()
+                conf = p.config(conf)
+                p.filter(conf, req, reps)
+            except AttributeError as e:
+                r.log.error("execute plugin `%s` AttributeError, %s" % (name, 
e.args.__str__()))
+                return False
+            except TypeError as e:
+                r.log.error("execute plugin `%s` TypeError, %s" % (name, 
e.args.__str__()))
+                return False
+            except BaseException as e:
+                r.log.error("execute plugin `%s` AnyError, %s" % (name, 
e.args.__str__()))
+                return False
+            else:
+                if reps.changed():
+                    break
+        return True
diff --git a/apisix/runner/server/handle.py b/apisix/runner/server/handle.py
index c79f1e3..9f43719 100644
--- a/apisix/runner/server/handle.py
+++ b/apisix/runner/server/handle.py
@@ -16,7 +16,7 @@
 #
 
 import flatbuffers
-import apisix.runner.plugin.core as runner_plugin
+from apisix.runner.plugin.core import PluginProcess as runner_plugin
 import apisix.runner.plugin.cache as runner_cache
 import apisix.runner.utils.common as runner_utils
 from apisix.runner.http.response import Response as NewHttpResponse
diff --git a/bin/py-runner b/bin/py-runner
index 9ec88aa..2731211 100755
--- a/bin/py-runner
+++ b/bin/py-runner
@@ -20,8 +20,9 @@
 import os
 import click
 
-from apisix.runner.server.server import Server as NewServer
-from apisix.runner.server.config import Config as NewConfig
+from apisix.runner.server.server import Server as RunnerServer
+from apisix.runner.server.config import Config as RunnerConfig
+from apisix.runner.plugin.core import PluginProcess as RunnerPlugin
 
 RUNNER_VERSION = "0.1.0"
 
@@ -34,8 +35,9 @@ def runner() -> None:
 
 @runner.command()
 def start() -> None:
-    config = 
NewConfig(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-    server = NewServer(config)
+    RunnerPlugin.register()
+    config = 
RunnerConfig(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+    server = RunnerServer(config)
     server.receive()
 
 
diff --git a/tests/runner/plugin/test_base.py b/tests/runner/plugin/test_base.py
deleted file mode 100644
index df7192f..0000000
--- a/tests/runner/plugin/test_base.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-from apisix.runner.plugin.base import Base
-
-
-def test_base():
-    hello_name = "hello"
-    hello_config = {"body": "apisix"}
-    hello = Base(hello_name)
-    hello.config = hello_config
-    assert hello.name == hello_name
-    assert hello.config == hello_config
-    hello.name = "hello1"
-    assert hello.name != hello_name
-
-    world_name = "world"
-    world_config = "apisxi"
-    world = Base(world_name)
-    world.config = world_config
-    assert world.name == world_name
-    assert world.config != world_config
-    world.name = "world1"
-    assert world.name != world_name
diff --git a/tests/runner/plugin/test_core.py b/tests/runner/plugin/test_core.py
index eff0b20..5c54d88 100644
--- a/tests/runner/plugin/test_core.py
+++ b/tests/runner/plugin/test_core.py
@@ -15,63 +15,111 @@
 # limitations under the License.
 #
 
-import os
 import socket
 import logging
-from pkgutil import iter_modules
 
-from apisix.runner.plugin.core import loading as plugin_loading
-from apisix.runner.plugin.core import execute as plugin_execute
+from apisix.runner.plugin.core import PluginBase
+from apisix.runner.plugin.core import PluginProcess
+from apisix.runner.plugin.core import PLUGINS
 from apisix.runner.server.logger import Logger as RunnerServerLogger
 from apisix.runner.server.server import RPCRequest as RunnerRPCRequest
 from apisix.runner.http.request import Request as NewHttpRequest
 from apisix.runner.http.response import Response as NewHttpResponse
 
 
-def test_loading():
-    configs = plugin_loading()
-    assert isinstance(configs, dict)
-    config_keys = configs.keys()
-    path = "%s/plugins" % 
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
-    modules = iter_modules(path=[path])
-    for _, name, _ in modules:
-        assert name in config_keys
+class NonePlugin:
+    pass
 
 
-def test_execute():
+class ErrorPlugin:
+
+    def config(self, conf):
+        return conf
+
+    def filter(self, conf, req, reps):
+        raise RuntimeError("Runtime Error")
+
+
+def default_request():
     sock = socket.socket()
     logger = RunnerServerLogger(logging.INFO)
-    r = RunnerRPCRequest(sock, logger)
+    return RunnerRPCRequest(sock, logger)
+
+
+def test_process_register():
+    assert PLUGINS == {}
+    PluginProcess.register()
+    assert len(PLUGINS) == 2
+
+
+def test_process_execute():
+    r = default_request()
     request = NewHttpRequest(r)
     response = NewHttpResponse()
-    configs = plugin_loading()
-    for p_name in configs:
-        configs[p_name] = configs.get(p_name)()
-    ok = plugin_execute(configs, r, request, response)
-    assert ok
-    # stop plugin
-    assert response.headers.get("X-Resp-A6-Runner") == "Python"
-    assert response.body == "Hello, Python Runner of APISIX"
-    assert response.status_code == 201
-    # rewrite plugin
-    assert request.headers.get("X-Resp-A6-Runner") == "Python"
-    assert request.args.get("a6_runner") == "Python"
-    assert request.path == "/a6/python/runner"
-    configs = {"test": {}}
-    ok = plugin_execute(configs, r, request, response)
-    assert not ok
-
-    class AttributeErrorExample:
-        pass
-
-    configs = {AttributeErrorExample.__name__.lower(): AttributeErrorExample()}
-    ok = plugin_execute(configs, r, request, response)
-    assert not ok
-
-    class TypeErrorExample:
-        def __init__(self):
-            self.filter = 10
-
-    configs = {TypeErrorExample.__name__.lower(): TypeErrorExample()}
-    ok = plugin_execute(configs, r, request, response)
-    assert not ok
+    tests = [
+        {
+            "conf": {
+                "stop": "config"
+            },
+            "autoload": True,
+            "req": request,
+            "resp": response,
+            "expected": True
+        },
+        {
+            "conf": {
+                "rewrite": "config"
+            },
+            "autoload": True,
+            "req": request,
+            "resp": response,
+            "expected": True
+        },
+        # AnyError
+        {
+            "conf": {
+                "any": "config"
+            },
+            "plugins": {
+                "any": ErrorPlugin
+            },
+            "autoload": False,
+            "expected": False
+        },
+        # AttributeError
+        {
+            "conf": {
+                "attr": "config"
+            },
+            "plugins": {
+                "attr": NonePlugin
+            },
+            "autoload": False,
+            "expected": False
+        },
+        # TypeError
+        {
+            "conf": {
+                "none": "config"
+            },
+            "autoload": True,
+            "expected": False
+        },
+    ]
+
+    for test in tests:
+        if test.get("autoload"):
+            PluginProcess.register()
+        if test.get("plugins"):
+            for plg_name, plg_obj in test.get("plugins").items():
+                PLUGINS[plg_name] = plg_obj
+        res = PluginProcess.execute(test.get("conf"), r, test.get("req"), 
test.get("resp"))
+        assert res == test.get("expected")
+
+
+def test_base():
+    r = default_request()
+    pb = PluginBase()
+    assert pb.name() is None
+    assert pb.config(None) is None
+    assert pb.filter(None, NewHttpRequest(r), NewHttpResponse()) is None

Reply via email to