Skip to content

Python-Tools/schema_entry

Repository files navigation

简介

程序入口的构造工具.

这个基类的设计目的是为了配置化入口的定义.通过继承和覆盖基类中的特定字段和方法来实现入口的参数配置读取.

目前的实现可以依次从指定路径下的json文件,环境变量,命令行参数读取需要的数据. 然后校验是否符合设定的json schema规定的模式,在符合模式后执行注册进去的回调函数.

入口树中可以有中间节点,用于分解复杂命令行参数,中间节点不会执行. 他们将参数传递给下一级节点,直到尾部可以执行为止.

特性

  • 根据子类的名字小写构造命令
  • 根据子类的docstring,epilog字段description字段自动构造,命令行说明.
  • 根据子类的schema字段env_prefix字段自动构造环境变量的读取规则.
  • 根据子类的default_config_file_paths字段自动按顺序读取json,yaml格式配置文件中的参数.
  • 根据schema字段构造命令行参数和配置校验
  • 使用装饰器@as_main注册获取到配置后执行的函数
  • 通过覆写parse_commandline_args方法来定义命令行参数的读取
  • 入口节点可以通过方法regist_sub注册子节点

安装

pip install schema_entry

使用介绍

动机

schema_entry模块提供了一个基类EntryPoint用于构造复杂的程序入口.通常我们的程序入口参数有3个途径:

  1. 配置文件
  2. 环境变量
  3. 命令行参数

在docker广泛应用之前可能用的最多的是命令行参数.但在docker大行其道的现在,配置文件(docker config)和环境变量(environment字段)变得更加重要.

随之而来的是参数的校验问题,python标准库argparse本身有不错的参数约束能力,但配置文件中的和环境变量中的参数就需要额外校验了.

这个项目的目的是简化定义入口这个非常通用的业务,将代码尽量配置化.

使用方法

首先我们来分析下一个入口形式.

通常一个程序的入口可能简单也可能复杂,但无非两种

  1. 中间节点,比如docker stack, 它本质上并不执行操作,它只是表示要执行的是关于子模块的操作.当单独执行这条命令时实际上它什么都没做,它下面的子命令git submodule add这类才是实际可以执行的节点.而我定义这种中间节点单独被执行应该打印其帮助说明文本.
  2. 执行节点,比如docker run,这种就是可以执行的节点.

本模块的基本用法是:

  1. 通过继承EntryPoint类并覆写其中的字段来定义不同的节点

  2. 通过实例化EntryPoint的子类并使用其实例方法regist_subcmd或者regist_sub来定义不同节点的类型和节点的调用顺序

  3. 使用可以执行节点的实例方法as_main(装饰器)来指定不同节点的入口函数.

  4. 命令行中按根节点可以执行节点的顺序输入构造命令,获取来自配置文件,环境变量,命令行参数中的参数,作为注册入口函数的参数调用入口函数.

节点名

我们可以定义_name字段为节点命名,如果没有那么节点名则为子类类名的全小写形式.

节点的帮助信息

我们可以定义usage来定义用法帮助字符串,如果没有定义则会自动构造,中间节点会是root subcmd ... [subcmd]; 可执行节点会是root subcmd ... entry [options]

执行节点

上面说过执行节点的任务有3个:

  1. 从配置文件,环境变量,命令行参数获取配置参数
  2. [可选]校验配置参数是否符合要求
  3. [可选]将配置作为参数引用到程序中.

通过定义schema字段进行参数校验

我们可以定义schema字段来激活校验功能

class Test_A(EntryPoint):
    default_config_file_paths = [
        "/test_config.json",
        str(Path.home().joinpath(".test_config.json")),
        "./test_config.json"
    ]
    schema = {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "a": {
                "type": "integer"
            }
        },
        "required": ["a"]
    }

EntryPoint的子类会在解析获得参数后校验参数字典是否符合schema中定义的模式.

当然schema字段也不能乱写,它的规则是json schema的一个子集:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
        "properties": {
            "type": "object",
            "minProperties": 1,
            "additionalProperties": False,
            "patternProperties": {
                "^\\w+$": {
                    "oneOf": [
                        {
                            "type": "object",
                            "additionalProperties": False,
                            "required": ["type"],
                            "properties": {
                                "type": {
                                    "type": "string",
                                    "const": "boolean"
                                },
                                "default": {
                                    "type": "boolean",
                                },
                                "const": {
                                    "type": "string"
                                },
                                "description": {
                                    "type": "string"
                                },
                                "$comment": {
                                    "type": "string"
                                },
                                "title": {
                                    "type": "string",
                                    "pattern": "^[a-b]|[d-z]$"
                                }
                            }
                        },
                        {
                            "type": "object",
                            "additionalProperties": false,
                            "required": ["type"],
                            "properties": {
                                "type": {
                                    "type": "string",
                                    "const": "string"
                                },
                                "default": {
                                    "type": "string",
                                },
                                "const": {
                                    "type": "string"
                                },
                                "enum": {
                                    "type": "array",
                                    "items": {
                                        "type": "string"
                                    }
                                },
                                "maxLength": {
                                    "type": "integer",
                                    "minimum": 0
                                },
                                "minLength": {
                                    "type": "integer",
                                    "minimum": 0
                                },
                                "pattern": {
                                    "type": "string"
                                },
                                "format": {
                                    "type": "string"
                                },
                                "description": {
                                    "type": "string"
                                },
                                "$comment": {
                                    "type": "string"
                                },
                                "title": {
                                    "type": "string",
                                    "pattern": r"^[a-b]|[d-z]$"
                                }
                            }
                        },
                        {
                            "type": "object",
                            "additionalProperties": false,
                            "required": ["type"],
                            "properties": {
                                "type": {
                                    "type": "string",
                                    "const": "number"
                                },
                                "default": {
                                    "type": "number",
                                },
                                "const": {
                                    "type": "number"
                                },
                                "enum": {
                                    "type": "array",
                                    "items": {
                                        "type": "number"
                                    }
                                },
                                "maximum": {
                                    "type": "number",
                                },
                                "exclusiveMaximum": {
                                    "type": "number",
                                },
                                "minimum": {
                                    "type": "number",

                                },
                                "exclusiveMinimum": {
                                    "type": "number",

                                },
                                "description": {
                                    "type": "string"
                                },
                                "$comment": {
                                    "type": "string"
                                },
                                "title": {
                                    "type": "string",
                                    "pattern": "^[a-b]|[d-z]$"
                                }
                            }
                        },
                        {
                            "type": "object",
                            "additionalProperties": false,
                            "required": ["type"],
                            "properties": {
                                "type": {
                                    "type": "string",
                                    "const": "integer"
                                },
                                "default": {
                                    "type": "integer",
                                },
                                "const": {
                                    "type": "integer"
                                },
                                "enum": {
                                    "type": "array",
                                    "items": {
                                        "type": "integer"
                                    }
                                },
                                "maximum": {
                                    "type": "integer",
                                },
                                "exclusiveMaximum": {
                                    "type": "integer",
                                },
                                "minimum": {
                                    "type": "integer",

                                },
                                "exclusiveMinimum": {
                                    "type": "integer",

                                },
                                "description": {
                                    "type": "string"
                                },
                                "$comment": {
                                    "type": "string"
                                },
                                "title": {
                                    "type": "string",
                                    "pattern": "^[a-b]|[d-z]$"
                                }
                            }
                        },
                        {
                            "type": "object",
                            "additionalProperties": false,
                            "required": ["type"],
                            "properties": {
                                "type": {
                                    "type": "string",
                                    "const": "array"
                                },
                                "default": {
                                    "type": "array",
                                    "items": {
                                        "type": ["string", "number", "integer"]
                                    }
                                },
                                "items": {
                                    "type": "object",
                                    "required": ["type"],
                                    "additionalProperties": false,
                                    "properties": {
                                        "type": {
                                            "type": "string",
                                            "enum": ["string", "number", "integer"]
                                        },
                                        "enum":{
                                            "type": "array"
                                        }
                                    }
                                },
                                "description": {
                                    "type": "string"
                                },
                                "$comment": {
                                    "type": "string"
                                },
                                "title": {
                                    "type": "string",
                                    "pattern": "^[a-b]|[d-z]$"
                                }
                            }
                        }
                    ]

                }
            }
        },
        "type": {
            "type": "string",
            "const": "object"
        },
        "required": {
            "type": "array",
            "items": {
                "type": "string"
            }
        }

    },
    "required": ["properties", "type"]
}

简而言之就是:

  1. 最外层必须有propertiestype字段且type字段必须为object,可以有required字段
  2. 最外层properties中的字段名必须是由数字,字母_组成,
  3. 字段类型只能是string,boolean,number,integer,array之一
  4. 字段类型如果为array则内部必须要有itemsitems中必须有type字段,且该type字段的值必须为string,number,integer之一

如果我们不想校验,那么可以设置verify_schemaFalse强行关闭这个功能.

从定义的schema中获取默认配置

我们在定义schema时可以在"properties"字段定义的模式描述中通过default字段指定描述字段的默认值

class Test_A(EntryPoint):
    schema = {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "a_a": {
                "type": "number"
                "default": 10.1
            }
        },
        "required": ["a_a"]
    }

这样即便没有其他输入这个参数也会有这个默认值兜底

从指定配置文件中读取配置

我们可以使用字段default_config_file_paths指定从固定的几个路径中读取配置文件,配置文件支持jsonyaml两种格式. 我们也可以通过字段config_file_only_get_need定义从配置文件中读取配置的行为(默认为True), 当置为True时我们只会在配置文件中读取schema中定义的字段,否则则会加载全部字段.

也可以通过设置load_all_config_file = True来按设定顺序读取全部预设的配置文件位置

默认配置文件地址是一个列表,会按顺序查找读取,只要找到了满足条件的配置文件就会读取.

from pathlib import Path
from schema_entry import EntryPoint

class Test_A(EntryPoint):

    default_config_file_paths = [
        "/test_config.json",
        str(Path.home().joinpath(".test_config.json")),
        "./test_config.json",
        "./test_config_other.json"
    ]
指定特定命名的配置文件的解析方式

可以使用@regist_config_file_parser(config_file_name)来注册如何解析特定命名的配置文件.这一特性可以更好的定制化配置文件的读取

class Test_AC(EntryPoint):
    load_all_config_file = True
    default_config_file_paths = [
        "./test_config.json",
        "./test_config1.json",
        "./test_other_config2.json"
    ]
root = Test_AC()

@root.regist_config_file_parser("test_other_config2.json")
def _1(p: Path) -> Dict[str, Any]:
    with open(p) as f:
        temp = json.load(f)
    return {k.lower(): v for k, v in temp.items()}

如果想在定义子类时固定好,也可以定义_config_file_parser_map:Dict[str,Callable[[Path], Dict[str, Any]]]

def test_other_config2_parser( p: Path) -> Dict[str, Any]:
    with open(p) as f:
        temp = json.load(f)
    return {k.lower(): v for k, v in temp.items()}
class Test_AC(EntryPoint):
    load_all_config_file = True
    default_config_file_paths = [
        "./test_config.json",
        "./test_config1.json",
        "./test_other_config2.json"
    ]
    _config_file_parser_map = {
        "test_other_config2.json": test_other_config2_parser
    }

root = Test_AC()

从环境变量中读取配置参数

要从环境变量中读取配置必须设置schema字段,EntryPoint会按照其中properties字段定义的字段范围和字段类型解析环境变量.

环境变量key的规则为前缀_字段名的大写.前缀的默认值为...父节命令节点的父命令节点大写_父节命令节点大写_子命令节点大写. 我们也可以通过设定env_prefix字段来替换默认前缀,替换的前缀依然会被转化为大写.

class Test_A(EntryPoint):
    env_prefix = "app"
    schema = {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "a_a": {
                "type": "number"
            }
        },
        "required": ["a_a"]
    }

如果我们不希望从环境变量中解析配置,那么也可以设置parse_envFalse

从命令行参数中获取配置参数

当我们定义好schema后所有schema中定义好的参数都可以以--xxxx的形式从命令行中读取,需要注意schema中定义的字段中_会被修改为-. 如果定义的字段模式中含有title字段,则使用title字段作为命令行缩写即-x的形式

这个命令行读取是使用的标准库argparse,构造出的解析器中useage,epilogdescription会由类中定义的usage,epilog和docstring决定;argv则为传到节点处时剩下的命令行参数(每多一个节点就会从左侧摘掉一个命令行参数).

通常情况下构造的命令行解析器全部都是可选项,如果我们希望指定schema中一项是没有--的那种配置,那么可以在定义类时指定argparse_noflag为想要的字段,如果希望命令行中校验必填项则可以在定义类时指定argparse_check_required=True.需要注意如果一个字段被指定为了noflag那么它就是必填项了.

class Test_A(EntryPoint):
    argparse_noflag = "a"
    argparse_check_required=True
    schema = {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "a": {
                "type": "number"
            },
            "b": {
                "type": "number"
            }
        },
        "required": ["a","b"]
    }

命令行中默认使用-c/--config来指定读取配置文件,它的读取行为受上面介绍的从自定义配置文件中读取配置的设置影响.

配置的读取顺序

配置的读取顺序为schema中定义的default值->配置指定的配置文件路径->命令行指定的配置文件->环境变量->命令行参数,而覆盖顺序则是反过来.

注册入口的执行函数

我们使用实例的装饰器方法as_main来实现对执行节点入口函数的注册,注册的入口函数会在解析好参数后执行,其参数就是解析好的**config

root = Test_A()
@root.as_main
def main(a,b):
    print(a)
    print(b)

另一种指定入口函数的方法是重写子类的do_main(self)->None方法

class Test_A(EntryPoint):
    argparse_noflag = "a"
    argparse_check_required=True
    schema = {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "a": {
                "type": "number"
            },
            "b": {
                "type": "number"
            }
        },
        "required": ["a","b"]
    }
    def do_main(self)->None:
        print(self.config)

直接从节点对象中获取配置

节点对象的config属性会在每次调用时copy一份当前的配置值,config是不可写的.

print(root.config)

中间节点

中间节点并不能执行程序,它只是用于描述一个范围内的命令集合,因此它的作用就是充当help指令.我们定义中间节点并不能执行.但必须有至少一个子节点才是中间节点.因此即便一个节点定义了上面的配置,只要它有子节点就不会按上面的执行流程执行.

利用中间节点我们可以构造出非常复杂的启动命令树.

注册子节点

中间节点的注册有两个接口

  • regist_subcmd用于注册一个已经实例化的子节点

    class A(EntryPoint):
        pass
    
    class B(EntryPoint):
        pass
    
    a = A()
    
    b = B()
    
    a.regist_subcmd(b)
  • regist_sub用于注册一个子节点类,它会返回被注册的节点的一个实例

    class A(EntryPoint):
        pass
    
    class B(EntryPoint):
        pass
    
    a = A()
    b =a.regist_sub(B)