跳转到主要内容

自动生成 __init__.py 文件

项目描述

mkinit

CircleCI Appveyor Codecov Pypi Downloads ReadTheDocs

阅读文档

https://mkinit.readthedocs.io

Github

https://github.com/Erotemic/mkinit

Pypi

https://pypi.ac.cn/project/mkinit

mkinit模块帮助您编写暴露所有子模块属性而不使用from ? import *__init__文件。

mkinit会自动导入一个包中的所有子模块及其成员。

它可以动态地做到这一点,也可以静态地自动生成__init__以实现更快的导入时间。它有点像使用fromimport *语法,但更容易替换为不会让其他开发者头疼的文本。

此模块支持科学Python SPEC1

请注意,本readme中的文档有些过时,需要更新以使最佳实践更加明确。您可以使用此模块的许多方法,但当前推荐的方法是使用

mkinit --lazy_loader <path-to-init.py>

安装

pip install mkinit

介绍

假设您有一个结构如下所示的Python模块

└── mkinit_demo_pkg
    ├── __init__.py
    ├── submod.py
    └── subpkg
        ├── __init__.py
        └── nested.py

并且您希望使submod.pynested.py中的所有函数在包的顶层可用。

想象一下submod.pynested.py的内容

# --- submod.py ---

def submod_func():
    print('This is a submod func in {}'.format(__file__))

# --- nested.py ---

def nested_func():
    print('This is a nested func in {}'.format(__file__))

您可以手动编写

from mkinit_demo_pkg.submod import *
from mkinit_demo_pkg.subpkg.nested import *

但这有几个问题。使用 import * 使得阅读代码的人难以知道来自哪里。此外,如果您想公开许多子模块的属性,编写这些内容会变得繁琐且难以维护。

这就是mkinit包的用武之地。它具有使用静态分析自动生成显式 __init__.py 文件的能力。通常,mkinit CLI一次只处理一个文件,但如果我们指定 --recursive 标志,那么mkinit将为包中的所有子包递归生成 __init__.py 文件。

因此,运行 mkinit mkinit_demo_pkg --recursive 将生成一个看起来像这样的根 __init__.py 文件

from mkinit_demo_pkg import submod
from mkinit_demo_pkg import subpkg

from mkinit_demo_pkg.submod import (submod_func,)
from mkinit_demo_pkg.subpkg import (nested, nested_func,)

__all__ = ['nested', 'nested_func', 'submod', 'submod_func', 'subpkg']

这非常酷。mkinit包能够递归解析我们的包,找到所有定义的名称,然后生成 __init__.py 文件,使得所有属性都在包的最高级别暴露。此外,这个文件是 可读的。无需执行任何操作就可以清楚地知道在这个模块中暴露了哪些名称。

当然,这并不是完美的解决方案。也许只应该公开一些子模块,也许你更愿意使用相对导入语句,也许你只想公开子模块而不公开它们的属性,或者相反。好消息是,mkinit有命令行标志可以支持所有这些模式。有关详细信息,请参阅 mkinit --help

最后,虽然公开所有属性对大型项目有帮助,但导入时间可能开始成为一个考虑因素。幸运的是,PEP 0562 为 Python >= 3.7 提出了懒加载导入规范。截至 2020-12-26,mkinit支持自动生成这些懒加载的初始化文件。

不幸的是,没有语法支持懒加载导入,所以mkinit必须在每个 __init__.py 文件中定义一个 lazy_import 模板函数。

def lazy_import(module_name, submodules, submod_attrs):
    """
    Boilerplate to define PEP 562 __getattr__ for lazy import
    https://pythonlang.cn/dev/peps/pep-0562/
    """
    import importlib
    import os
    name_to_submod = {
        func: mod for mod, funcs in submod_attrs.items()
        for func in funcs
    }

    def __getattr__(name):
        if name in submodules:
            attr = importlib.import_module(
                '{module_name}.{name}'.format(
                    module_name=module_name, name=name)
            )
        elif name in name_to_submod:
            submodname = name_to_submod[name]
            module = importlib.import_module(
                '{module_name}.{submodname}'.format(
                    module_name=module_name, submodname=submodname)
            )
            attr = getattr(module, name)
        else:
            raise AttributeError(
                'No {module_name} attribute {name}'.format(
                    module_name=module_name, name=name))
        globals()[name] = attr
        return attr

    if os.environ.get('EAGER_IMPORT', ''):
        for name in submodules:
            __getattr__(name)

        for attrs in submod_attrs.values():
            for attr in attrs:
                __getattr__(attr)
    return __getattr__


__getattr__ = lazy_import(
    __name__,
    submodules={
        'submod',
        'subpkg',
    },
    submod_attrs={
        'submod': [
            'submod_func',
        ],
        'subpkg': [
            'nested',
            'nested_func',
        ],
    },
)

def __dir__():
    return __all__

__all__ = ['nested', 'nested_func', 'submod', 'submod_func', 'subpkg']

不过,如果你愿意依赖于 lazy_loader 包和 --lazy_loader 选项(从1.0.0版开始),那么这个模板函数就不再需要了。

默认情况下,懒加载导入与静态类型项目(例如使用mypy或pyright)不兼容,但是,如果使用 lazy_loader 包,可以指定 --lazy_loader_typed 选项来生成除了懒加载评估的 __init__.py 文件之外,还可以生成 __init__.pyi__ 文件。这些接口文件被静态类型检查器理解,并允许将懒加载与静态类型检查相结合。

命令行用法

以下命令将在指定的路径或模块名称中静态自动生成一个 __init__ 文件。如果已存在,它将只替换最后一条注释后的文本。这意味着 mkinit 不会覆盖你的自定义逻辑,并可用于帮助维护自定义的 __init__.py 文件。

mkinit <your_modname_or_modpath> -w

你还可以用特殊的xml-like注释将允许被覆盖的自动生成区域包围起来。

运行 mkinit --help 显示

usage: python -m mkinit [-h] [--dry] [-i] [--diff] [--noattrs] [--nomods] [--noall] [--relative] [--lazy | --lazy_loader] [--black] [--lazy_boilerplate LAZY_BOILERPLATE] [--recursive] [--norespect_all]
                        [--verbose [VERBOSE]] [--version]
                        [modname_or_path]

Autogenerate an `__init__.py` that exposes a top-level API.

Behavior is modified depending on the existing content of the
`__init__.py` file (subsequent runs of mkinit are idempotent).

The following `__init__.py` variables modify autogeneration behavior:

    `__submodules__` (List[str] | Dict[str, List[str])) -
        Indicates the list of submodules to be introspected, if
        unspecified all submodules are introspected. Can be a list
        of submodule names, or a dictionary mapping each submodule name
        to a list of attribute names to expose. If the value is None,
        then all attributes are exposed (or __all__) is respected).

    `__external__` - Specify external modules to expose the attributes of.

    `__explicit__` - Add custom explicitly defined names to this, and
        they will be automatically added to the __all__ variable.

    `__protected__` -  Protected modules are exposed, but their attributes are not.

    `__private__` - Private modules and their attributes are not exposed.

    `__ignore__` - Tells mkinit to ignore particular attributes

positional arguments:
  modname_or_path       module or path to generate __init__.py for

options:
  -h, --help            show this help message and exit
  --dry
  -i, -w, --write, --inplace
                        modify / write to the file inplace
  --diff                show the diff (forces dry mode)
  --noattrs             Do not generate attribute from imports
  --nomods              Do not generate modules imports
  --noall               Do not generate an __all__ variable
  --relative            Use relative . imports instead of <modname>
  --lazy                Use lazy imports with more boilerplate but no dependencies (Python >= 3.7 only!)
  --lazy_loader         Use lazy imports with less boilerplate but requires the lazy_loader module (Python >= 3.7 only!)
  --lazy_loader_typed   Use lazy imports with the lazy_loader module, additionally generating
                        ``__init__.pyi`` files for static typing (e.g. with mypy or pyright) (Python >= 3.7 only!)
  --black               Use black formatting
  --lazy_boilerplate LAZY_BOILERPLATE
                        Code that defines a custom lazy_import callable
  --recursive           If specified, runs mkinit on all subpackages in a package
  --norespect_all       if False does not respect __all__ attributes of submodules when parsing
  --verbose [VERBOSE]   Verbosity level
  --version             print version and exit

动态用法

注意:不推荐使用动态用法。

在大多数情况下,我们推荐使用mkinit命令行工具来静态生成/更新 __init__.py 文件,但有一个选项可以动态使用(尽管这可能会被认为比使用 import * 更差的做法)。

import mkinit; exec(mkinit.dynamic_init(__name__))

示例

ubelt 库使用 mkinit 模块来显式自动生成 __init__.py 文件的一部分。以下示例通过介绍该模块的设计,来说明 mkinit 的使用。

步骤 1(可选):编写任何自定义的 __init__ 代码

ubelt 模块的第一部分包含手动编写的代码。它包括代码、flake8 指令、一些注释、一个文档字符串、一个未来导入和一个自定义的 __version__ 属性。以下是在 ubelt0.2.0.dev0 版本中手动编写的代码示例。

# -*- coding: utf-8 -*-
# flake8: noqa
"""
CommandLine:
    # Partially regenerate __init__.py
    mkinit ubelt
"""
# Todo:
#     The following functions and classes are candidates to be ported from utool:
#     * reload_class
#     * inject_func_as_property
#     * accumulate
#     * rsync
from __future__ import absolute_import, division, print_function, unicode_literals

__version__ = '0.2.0'

上述代码的具体内容并不重要,关键是要说明 mkinit 并不会阻止您自定义代码。默认情况下,自动生成将仅在文件中的最后一个注释之后覆盖现有代码,这是一个相当好的启发式方法,但正如我们将看到的,还有其他更明确的方法来定义允许自动生成代码的确切位置。

步骤 2(可选):列举相关的子模块

在可选地编写任何自定义代码后,您可以指定在自动生成导入时应考虑哪些子模块。这是通过将 __submodules__ 属性设置为子模块名称的列表来实现的。

ubelt 中,这部分看起来类似于以下内容

__submodules__ = [
    'util_arg',
    'util_cmd',
    'util_dict',
    'util_links',
    'util_hash',
    'util_import',
    'orderedset',
    'progiter',
]

请注意,这一步是可选的,但建议这样做。如果没有指定 __submodules__ 包,则所有与 *.py*/__init__.py 匹配的路径都被视为包的一部分。

步骤 3:显式自动生成

为了提供最快的导入时间和最易于阅读的 __init__.py 文件,请使用 mkinit 命令行脚本来静态分析子模块,并将子模块及其顶级成员填充到 __init__.py 文件中。

在运行此脚本之前,将 XML 类似的注释指令粘贴到 __init__.py 文件中是一个好习惯。这限制了 mkinit 允许自动生成代码的位置,并且如果需要有条件地运行自动生成的代码,它还使用了注释的相同缩进。请注意,如果没有指定第二个标签,则假定 mkinit 可以覆盖第一个标签之后的所有内容。

# <AUTOGEN_INIT>
pass
# </AUTOGEN_INIT>

现在我们已经插入了自动生成的标签,我们可以实际运行 mkinit。通常,这是通过运行 mkinit <path-to-pkg-directory> 来实现的。

假设 ubelt 仓库已检出在 ~/code/ 中,自动生成其 __init__.py 文件的命令将是:mkinit ~/code/ubelt/ubelt。根据之前指定的 __submodules__,自动生成的代码部分看起来像这样

# <AUTOGEN_INIT>
from ubelt import util_arg
from ubelt import util_cmd
from ubelt import util_dict
from ubelt import util_links
from ubelt import util_hash
from ubelt import util_import
from ubelt import orderedset
from ubelt import progiter
from ubelt.util_arg import (argflag, argval,)
from ubelt.util_cmd import (cmd,)
from ubelt.util_dict import (AutoDict, AutoOrderedDict, ddict, dict_hist,
                             dict_subset, dict_take, dict_union, dzip,
                             find_duplicates, group_items, invert_dict,
                             map_keys, map_vals, odict,)
from ubelt.util_links import (symlink,)
from ubelt.util_hash import (hash_data, hash_file,)
from ubelt.util_import import (import_module_from_name,
                               import_module_from_path, modname_to_modpath,
                               modpath_to_modname, split_modpath,)
from ubelt.orderedset import (OrderedSet, oset,)
from ubelt.progiter import (ProgIter,)
__all__ = ['util_arg', 'util_cmd', 'util_dict', 'util_links', 'util_hash',
           'util_import', 'orderedset', 'progiter', 'argflag', 'argval', 'cmd',
           'AutoDict', 'AutoOrderedDict', 'ddict', 'dict_hist', 'dict_subset',
           'dict_take', 'dict_union', 'dzip', 'find_duplicates', 'group_items',
           'invert_dict', 'map_keys', 'map_vals', 'odict', 'symlink',
           'hash_data', 'hash_file', 'import_module_from_name',
           'import_module_from_path', 'modname_to_modpath',
           'modpath_to_modname', 'split_modpath', 'OrderedSet', 'oset',
           'ProgIter']

在运行命令行 mkinit 工具时,将使用静态分析检查目标模块,因此从未运行过目标模块的任何代码。这避免了意外的副作用,防止了任意代码执行,并确保即使存在运行时错误,mkinit 也会做一些有用的事情。

步骤 3(替代):动态自动生成

从命令行运行 mkinit 可以生成最干净、最易读的 __init__.py 文件,但每次修改库时都需要运行它。这在快速开发新 Python 包时并不总是希望的。在这种情况下,可以在导入模块时动态执行 mkinit。要使用动态初始化,只需将以下行粘贴到 __init__.py 文件中。

import mkinit
exec(mkinit.dynamic_init(__name__, __submodules__))

这几乎等同于运行静态命令行变体。然而,它不是使用静态分析,而是使用 Python 解释器来执行和导入所有子模块,并动态检查定义的成员。这比使用静态分析更快,并且在大多数情况下,导入的属性结果不会有差异。为了避免所有差异,请在每个子模块中指定 __all__ 属性。

注意,包含 __submodules__ 属性并不是绝对必要的。如果未明确指定为参数,则动态版本的此函数将查找父堆栈帧中的此属性。

还可以使用条件逻辑来实现“两者兼得”的折衷方案。使用条件块来执行动态初始化,并将静态自动生成标签放置在未执行的块中。这可以让您在不担心更新 __init__.py 文件的情况下进行开发,并在需要时为文档目的静态生成代码。一旦快速开发阶段结束,您可以删除动态条件,保留自动生成的部分,并且忘记您曾经使用过 mkinit

__DYNAMIC__ = True
if __DYNAMIC__:
    from mkinit import dynamic_mkinit
    exec(dynamic_mkinit.dynamic_init(__name__))
else:
    # <AUTOGEN_INIT>
    from mkinit import dynamic_mkinit
    from mkinit import static_mkinit
    from mkinit.dynamic_mkinit import (dynamic_init,)
    from mkinit.static_mkinit import (autogen_init,)
    # </AUTOGEN_INIT>

行为说明

mkinit 模块是执行复杂任务的一种简单方法。有时它可能感觉像是魔法,尽管我向您保证它不是。为了最大限度地减少魔法的感觉并最大限度地了解其行为,请考虑以下内容

  • 在发现子模块的属性时,mkinit 将默认尊重 __all__ 属性。通常,指定此属性是一种良好的做法;这样做还可以避免以下注意事项。

  • 静态分析目前仅提取顶级模块属性。但是,它还会提取在条件 if-else 或 try-except 语句的所有非错误引发路径上定义的属性。

  • 静态分析目前不检查或不考虑 del 操作符的使用。同样,这些将由动态分析来考虑。

  • 在没有 __init__.py 文件的情况下,mkinit 命令行工具将创建一个。

  • 默认情况下,我们忽略以单个下划线标记为非公共的属性

待办事项

  • [ ] 给 dynamic_init 一个选项字典,以与 static_init 保持兼容的 API。

  • [ ] 如果一个属性会被定义两次,那么就根本不要定义它。目前,它已经定义了,但它的值并不明确。

项目详情


下载文件

下载适合您平台的文件。如果您不确定要选择哪个,请了解有关 安装包 的更多信息。

源分发

mkinit-1.1.0.tar.gz (63.7 kB 查看哈希)

上传时间 源代码

构建发行版

mkinit-1.1.0-py3-none-any.whl (63.1 kB 查看哈希)

上传时间 Python 3

支持者