├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── README_zh.md ├── dev-requirements.txt ├── flask_seek ├── __init__.py ├── factory.py ├── register.py └── util.py ├── pyproject.toml ├── pyrightconfig.json └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .vscode/ 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ShangSky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-seek 2 | 3 | - [简体中文](README_zh.md) 4 | 5 | An flask extension to make your code more elegant. 6 | 7 | Automatically discover and register Blueprint and decorators (such as before_request). 8 | 9 | ## Requirements 10 | 11 | - Python 3.6+ 12 | - Flask 1.1.0+ 13 | 14 | ## Installation 15 | 16 | ```shell 17 | $ pip install flask-seek 18 | ``` 19 | 20 | ## A Simple Example 21 | 22 | - Project structure and content 23 | 24 | ```shell 25 | project 26 | hello.py 27 | main.py 28 | ``` 29 | 30 | ```python 31 | # main.py 32 | from flask import Flask 33 | from flask_seek import seek 34 | 35 | app = Flask(__name__) 36 | 37 | 38 | seek(app, blueprint_modules=["hello"]) 39 | 40 | if __name__ == "__main__": 41 | app.run() 42 | ``` 43 | 44 | ```python 45 | # hello.py 46 | from flask import Blueprint 47 | 48 | hello_bp = Blueprint("hello", __name__) 49 | 50 | 51 | @hello_bp.route("/") 52 | def hello(): 53 | return {"msg": "Hello"} 54 | ``` 55 | 56 | - start 57 | 58 | ``` 59 | $ python main.py 60 | * Serving Flask app 'main' (lazy loading) 61 | * Environment: production 62 | WARNING: This is a development server. Do not use it in a production deployment. 63 | Use a production WSGI server instead. 64 | * Debug mode: off 65 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 66 | ``` 67 | 68 | ```shell 69 | $ curl -s http://127.0.0.1:5000/ 70 | {"msg":"Hello"} 71 | ``` 72 | 73 | ## Example upgrade 74 | 75 | ```python 76 | project 77 | common 78 | __init__.py 79 | error_handler.py 80 | middleware.py 81 | controller 82 | __init__.py 83 | hello.py 84 | main.py 85 | ``` 86 | 87 | ```python 88 | # main.py 89 | from flask import Flask 90 | from flask_seek import seek 91 | 92 | app = Flask(__name__) 93 | 94 | 95 | seek(app, blueprint_deep_modules=["controller"], decorator_modules=["common"]) 96 | 97 | if __name__ == "__main__": 98 | app.run() 99 | ``` 100 | 101 | ```python 102 | # hello.py 103 | from flask import Blueprint 104 | 105 | hello_bp = Blueprint("hello", __name__) 106 | 107 | 108 | @hello_bp.route("/") 109 | def hello(): 110 | print("hello") 111 | return {"msg": "Hello"} 112 | 113 | @hello_bp.route("/error") 114 | def error(): 115 | a = 1 / 0 116 | return {"msg": "Hello"} 117 | ``` 118 | 119 | ```python 120 | # error_handler.py 121 | from flask_seek import ff 122 | 123 | 124 | @ff.errorhandler(Exception) 125 | def err(e): 126 | return {"msg": "Server Error"} 127 | ``` 128 | 129 | ```python 130 | # middlerware.py 131 | from flask_seek import df 132 | 133 | 134 | @df.before_request 135 | def before(): 136 | print("before_request") 137 | 138 | 139 | @df.after_request 140 | def after(resp): 141 | print("after_request") 142 | return resp 143 | ``` 144 | 145 | - start 146 | 147 | ```shell 148 | $ python main.py 149 | * Serving Flask app 'main' (lazy loading) 150 | * Environment: production 151 | WARNING: This is a development server. Do not use it in a production deployment. 152 | Use a production WSGI server instead. 153 | * Debug mode: off 154 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 155 | ``` 156 | 157 | - Blueprint registered automatically 158 | 159 | ```shell 160 | $ curl -s http://127.0.0.1:5000/ 161 | {"msg":"Hello"} 162 | ``` 163 | 164 | - before_request, after_request take effect 165 | 166 | ```shell 167 | $ python main.py 168 | * Serving Flask app 'main' (lazy loading) 169 | * Environment: production 170 | WARNING: This is a development server. Do not use it in a production deployment. 171 | Use a production WSGI server instead. 172 | * Debug mode: off 173 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 174 | before_request 175 | hello 176 | after_request 177 | 127.0.0.1 - - [11/Jun/2021 00:06:13] "GET / HTTP/1.1" 200 - 178 | ``` 179 | 180 | - errorhandler take effect 181 | 182 | ```shell 183 | $ curl -s http://127.0.0.1:5000/error 184 | {"msg":"Server Error"} 185 | ``` 186 | 187 | ## Guide 188 | 189 | ### seek 190 | 191 | - parameters 192 | 193 | - instance - flask or buleprint instance 194 | 195 | - blueprint_modules - List of blueprint modules path such as `["common", "common.demo"]` 196 | 197 | - blueprint_deep_modules - It will recursively query all blueprint modules of the package 198 | 199 | - decorator_modules - List of flask decorator modules path 200 | 201 | - decorator_deep_modules - It will recursively query all decorator modules of the package 202 | 203 | - example 204 | 205 | ``` 206 | project 207 | common 208 | __init__.py 209 | error_handler.py 210 | middleware.py 211 | demo 212 | __init__.py 213 | a.py 214 | main.py 215 | ``` 216 | 217 | ```python 218 | # main.py 219 | from flask import Flask 220 | from flask_seek import seek 221 | 222 | app = Flask(__name__) 223 | 224 | 225 | seek(app, decorator_modules=["common"]) # will search error_handler.py, middleware.py 226 | seek(app, decorator_modules=["common.middleware"]) # will search middleware.py 227 | seek(app, decorator_deep_modules=["common"]) # will search error_handler.py, middleware.py, a.py 228 | seek(app, decorator_modules=["common.demo"]) # will search a.py 229 | ``` 230 | 231 | ### df 232 | 233 | decorator without parameters 234 | 235 | ```python 236 | from flask_seek import df 237 | 238 | 239 | @df.before_request 240 | def before(): 241 | print("before_request") 242 | ``` 243 | 244 | ### ff 245 | 246 | decorator with parameters 247 | 248 | ```python 249 | from flask_seek import ff 250 | 251 | 252 | @ff.errorhandler(Exception) 253 | def err(e): 254 | return {"msg": "Server Error"} 255 | ``` 256 | 257 | ## License 258 | 259 | This project is licensed under the terms of the MIT license. 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # flask-seek 2 | 3 | - [English](README.md) 4 | 5 | 使代码更优雅的flask扩展. 6 | 7 | 自动发现并注册蓝图和flask装饰器(例如before_request). 8 | 9 | ## 依赖 10 | 11 | - Python 3.6+ 12 | - Flask 1.1.0+ 13 | 14 | ## 安装 15 | 16 | ```shell 17 | $ pip install flask-seek 18 | ``` 19 | 20 | ## 简单的例子 21 | 22 | - 项目结构和内容 23 | 24 | ```shell 25 | project 26 | hello.py 27 | main.py 28 | ``` 29 | 30 | ```python 31 | # main.py 32 | from flask import Flask 33 | from flask_seek import seek 34 | 35 | app = Flask(__name__) 36 | 37 | 38 | seek(app, blueprint_modules=["hello"]) 39 | 40 | if __name__ == "__main__": 41 | app.run() 42 | ``` 43 | 44 | ```python 45 | # hello.py 46 | from flask import Blueprint 47 | 48 | hello_bp = Blueprint("hello", __name__) 49 | 50 | 51 | @hello_bp.route("/") 52 | def hello(): 53 | return {"msg": "Hello"} 54 | ``` 55 | 56 | - 运行 57 | 58 | ``` 59 | $ python main.py 60 | * Serving Flask app 'main' (lazy loading) 61 | * Environment: production 62 | WARNING: This is a development server. Do not use it in a production deployment. 63 | Use a production WSGI server instead. 64 | * Debug mode: off 65 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 66 | ``` 67 | 68 | ```shell 69 | $ curl -s http://127.0.0.1:5000/ 70 | {"msg":"Hello"} 71 | ``` 72 | 73 | ## 示例 74 | 75 | ```python 76 | project 77 | common 78 | __init__.py 79 | error_handler.py 80 | middleware.py 81 | controller 82 | __init__.py 83 | hello.py 84 | main.py 85 | ``` 86 | 87 | ```python 88 | # main.py 89 | from flask import Flask 90 | from flask_seek import seek 91 | 92 | app = Flask(__name__) 93 | 94 | 95 | seek(app, blueprint_deep_modules=["controller"], decorator_modules=["common"]) 96 | 97 | if __name__ == "__main__": 98 | app.run() 99 | ``` 100 | 101 | ```python 102 | # hello.py 103 | from flask import Blueprint 104 | 105 | hello_bp = Blueprint("hello", __name__) 106 | 107 | 108 | @hello_bp.route("/") 109 | def hello(): 110 | print("hello") 111 | return {"msg": "Hello"} 112 | 113 | @hello_bp.route("/error") 114 | def error(): 115 | a = 1 / 0 116 | return {"msg": "Hello"} 117 | ``` 118 | 119 | ```python 120 | # error_handler.py 121 | from flask_seek import ff 122 | 123 | 124 | @ff.errorhandler(Exception) 125 | def err(e): 126 | return {"msg": "Server Error"} 127 | ``` 128 | 129 | ```python 130 | # middlerware.py 131 | from flask_seek import df 132 | 133 | 134 | @df.before_request 135 | def before(): 136 | print("before_request") 137 | 138 | 139 | @df.after_request 140 | def after(resp): 141 | print("after_request") 142 | return resp 143 | ``` 144 | 145 | - 运行 146 | 147 | ```shell 148 | $ python main.py 149 | * Serving Flask app 'main' (lazy loading) 150 | * Environment: production 151 | WARNING: This is a development server. Do not use it in a production deployment. 152 | Use a production WSGI server instead. 153 | * Debug mode: off 154 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 155 | ``` 156 | 157 | - 自动注册蓝图 158 | 159 | ```shell 160 | $ curl -s http://127.0.0.1:5000/ 161 | {"msg":"Hello"} 162 | ``` 163 | 164 | - before_request, after_request装饰器生效 165 | 166 | ```shell 167 | $ python main.py 168 | * Serving Flask app 'main' (lazy loading) 169 | * Environment: production 170 | WARNING: This is a development server. Do not use it in a production deployment. 171 | Use a production WSGI server instead. 172 | * Debug mode: off 173 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 174 | before_request 175 | hello 176 | after_request 177 | 127.0.0.1 - - [11/Jun/2021 00:06:13] "GET / HTTP/1.1" 200 - 178 | ``` 179 | 180 | - errorhandler装饰器生效 181 | 182 | ```shell 183 | $ curl -s http://127.0.0.1:5000/error 184 | {"msg":"Server Error"} 185 | ``` 186 | 187 | ## 指引 188 | 189 | ### seek 190 | 191 | - 参数 192 | 193 | - instance - Flask和Blueprint的实例 194 | 195 | - blueprint_modules - 蓝图模块路径列表, 例如 `["common", "common.demo"]` 196 | 197 | - blueprint_deep_modules - 递归查找的蓝图模块路径 198 | 199 | - decorator_modules - flask装饰器路径列表 200 | 201 | - decorator_deep_modules - 递归查找flask装饰器路径列表 202 | 203 | - 示例 204 | 205 | ``` 206 | project 207 | common 208 | __init__.py 209 | error_handler.py 210 | middleware.py 211 | demo 212 | __init__.py 213 | a.py 214 | main.py 215 | ``` 216 | 217 | ```python 218 | # main.py 219 | from flask import Flask 220 | from flask_seek import seek 221 | 222 | app = Flask(__name__) 223 | 224 | 225 | seek(app, decorator_modules=["common"]) # 会搜索到error_handler.py, middleware.py 226 | seek(app, decorator_modules=["common.middleware"]) # 会搜索到middleware.py 227 | seek(app, decorator_deep_modules=["common"]) # 会搜索到error_handler.py, middleware.py, a.py 228 | seek(app, decorator_modules=["common.demo"]) # 会搜索到a.py 229 | ``` 230 | 231 | ### df 232 | 233 | 装饰器 234 | 235 | ```python 236 | from flask_seek import df 237 | 238 | 239 | @df.before_request 240 | def before(): 241 | print("before_request") 242 | ``` 243 | 244 | ### ff 245 | 246 | 带参装饰器 247 | 248 | ```python 249 | from flask_seek import ff 250 | 251 | 252 | @ff.errorhandler(Exception) 253 | def err(e): 254 | return {"msg": "Server Error"} 255 | ``` 256 | 257 | ## 许可证 258 | 259 | 此项目使用MIT许可证. 260 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pyright 2 | black 3 | isort -------------------------------------------------------------------------------- /flask_seek/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_seek.factory import df, ff 2 | from flask_seek.register import seek 3 | 4 | __version__ = "0.2.1" 5 | __all__ = ["df", "ff", "seek"] 6 | -------------------------------------------------------------------------------- /flask_seek/factory.py: -------------------------------------------------------------------------------- 1 | from functools import update_wrapper 2 | from typing import TYPE_CHECKING, Any, Callable, Optional, Type, Union 3 | 4 | 5 | class MethodProxy: 6 | def __init__( 7 | self, 8 | f: Callable, 9 | method_name: str, 10 | args: Optional[tuple] = None, 11 | kwargs: Optional[dict] = None, 12 | ) -> None: 13 | self.f = f 14 | self.method_name = method_name 15 | self.args = args 16 | self.kwargs = kwargs 17 | update_wrapper(self, f) 18 | 19 | def __call__(self, *args: Any, **kwargs: Any) -> Any: 20 | return self.f(*args, **kwargs) 21 | 22 | 23 | def wrapper( 24 | method_name: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None 25 | ) -> Callable[[Callable], MethodProxy]: 26 | def inner(f: Callable) -> MethodProxy: 27 | return MethodProxy(f=f, method_name=method_name, args=args, kwargs=kwargs) 28 | 29 | return inner 30 | 31 | 32 | class DecoratorFactory: 33 | def __getattr__(self, item: str) -> Callable[[Callable], MethodProxy]: 34 | def decorator(f: Callable) -> MethodProxy: 35 | return MethodProxy(f=f, method_name=item) 36 | 37 | return decorator 38 | 39 | if TYPE_CHECKING: 40 | 41 | def before_first_request(self, f: Callable) -> Callable: 42 | ... 43 | 44 | def teardown_appcontext(self, f: Callable) -> Callable: 45 | ... 46 | 47 | def shell_context_processor(self, f: Callable) -> Callable: 48 | ... 49 | 50 | def before_request(self, f: Callable) -> Callable: 51 | ... 52 | 53 | def after_request(self, f: Callable) -> Callable: 54 | ... 55 | 56 | def teardown_request(self, f: Callable) -> Callable: 57 | ... 58 | 59 | def context_processor(self, f: Callable) -> Callable: 60 | ... 61 | 62 | def url_value_preprocessor(self, f: Callable) -> Callable: 63 | ... 64 | 65 | def url_defaults(self, f: Callable) -> Callable: 66 | ... 67 | 68 | 69 | class FunctionFactory: 70 | def __getattr__(self, item: str) -> Callable: 71 | def decorator(*args: Any, **kwargs: Any) -> Callable: 72 | return wrapper(item, args, kwargs) 73 | 74 | return decorator 75 | 76 | if TYPE_CHECKING: 77 | 78 | def template_filter(self, name: Optional[str] = None) -> Callable[[Callable], Callable]: 79 | ... 80 | 81 | def template_test(self, name: Optional[str] = None) -> Callable[[Callable], Callable]: 82 | ... 83 | 84 | def template_global(self, name: Optional[str] = None) -> Callable[[Callable], Callable]: 85 | ... 86 | 87 | def errorhandler( 88 | self, code_or_exception: Union[Type[Exception], int] 89 | ) -> Callable[[Callable], Callable]: 90 | ... 91 | 92 | 93 | df = DecoratorFactory() 94 | ff = FunctionFactory() 95 | -------------------------------------------------------------------------------- /flask_seek/register.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional, Union 2 | 3 | from flask import Blueprint, Flask 4 | 5 | from flask_seek.factory import MethodProxy 6 | from flask_seek.util import get_objs_in_modules 7 | 8 | 9 | def register_blueprints( 10 | instance: Union[Flask, Blueprint], pkg_names: Iterable[str], *, deep: bool = False 11 | ) -> None: 12 | for obj in get_objs_in_modules(pkg_names, Blueprint, deep=deep): 13 | instance.register_blueprint(obj) 14 | 15 | 16 | def register_methods( 17 | instance: Union[Flask, Blueprint], pkg_names: Iterable[str], *, deep: bool = False 18 | ) -> None: 19 | for obj in get_objs_in_modules(pkg_names, MethodProxy, deep=deep): 20 | method = getattr(instance, obj.method_name, None) 21 | if method is not None: 22 | if obj.args is None: 23 | method(obj.f) 24 | else: 25 | method(*obj.args, **obj.kwargs)(obj.f) 26 | 27 | 28 | def seek( 29 | instance: Union[Flask, Blueprint], 30 | *, 31 | blueprint_modules: Optional[Iterable[str]] = None, 32 | blueprint_deep_modules: Optional[Iterable[str]] = None, 33 | decorator_modules: Optional[Iterable[str]] = None, 34 | decorator_deep_modules: Optional[Iterable[str]] = None 35 | ) -> None: 36 | if blueprint_modules: 37 | register_blueprints(instance, blueprint_modules) 38 | if blueprint_deep_modules: 39 | register_blueprints(instance, blueprint_deep_modules, deep=True) 40 | if decorator_modules: 41 | register_methods(instance, decorator_modules) 42 | if decorator_deep_modules: 43 | register_methods(instance, decorator_deep_modules, deep=True) 44 | -------------------------------------------------------------------------------- /flask_seek/util.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | from importlib import import_module 3 | from types import ModuleType 4 | from typing import Any, Callable, Iterable, Optional, Type, TypeVar 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | def find_modules(pkg_name: str, *, deep: bool = False, lazy: bool = False) -> Iterable[ModuleType]: 10 | pkg = import_module(pkg_name) 11 | module_path = getattr(pkg, "__path__", None) 12 | if module_path is None: 13 | return [pkg] 14 | iter_modules_func = pkgutil.walk_packages if deep else pkgutil.iter_modules 15 | iter_modules = ( 16 | import_module(module_info[1]) 17 | for module_info in iter_modules_func(module_path, pkg.__name__ + ".") 18 | if not module_info[2] 19 | ) 20 | return iter_modules if lazy else list(iter_modules) 21 | 22 | 23 | def get_objs( 24 | module: ModuleType, cls: Type[T], func: Optional[Callable[[Any, Any], bool]] = None 25 | ) -> Iterable[T]: 26 | for attr_name in dir(module): 27 | if not attr_name.startswith("__"): 28 | attr = getattr(module, attr_name) 29 | call = func or isinstance 30 | if call(attr, cls): 31 | yield attr 32 | 33 | 34 | def import_string(dotted_path: str) -> Any: 35 | """ 36 | Import a dotted module path and return the attribute/class designated by the 37 | last name in the path. Raise ImportError if the import failed. 38 | """ 39 | try: 40 | module_path, class_name = dotted_path.rsplit(".", 1) 41 | except ValueError as err: 42 | raise ImportError("%s doesn't look like a module path" % dotted_path) from err 43 | 44 | module = import_module(module_path) 45 | 46 | try: 47 | return getattr(module, class_name) 48 | except AttributeError as err: 49 | raise ImportError( 50 | 'Module "{}" does not define a "{}" attribute/class'.format(module_path, class_name) 51 | ) from err 52 | 53 | 54 | def get_objs_in_modules( 55 | pkg_names: Iterable[str], 56 | cls: Type[T], 57 | *, 58 | deep: bool = False, 59 | func: Optional[Callable[[Any, Any], bool]] = None 60 | ) -> Iterable[T]: 61 | obj_ids = set() 62 | for pkg_name in pkg_names: 63 | modules = find_modules(pkg_name, deep=deep) 64 | for module in modules: 65 | for obj in get_objs(module, cls, func=func): 66 | obj_id = id(obj) 67 | if obj_id not in obj_ids: 68 | obj_ids.add(obj_id) 69 | yield obj 70 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | 4 | [tool.isort] 5 | line_length = 100 6 | force_grid_wrap = 0 7 | include_trailing_comma = true 8 | multi_line_output = 3 9 | use_parentheses = true 10 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "reportUnusedImport": "warning" 3 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding = utf-8 2 | import re 3 | 4 | from setuptools import find_packages, setup 5 | 6 | with open("flask_seek/__init__.py", "r") as f: 7 | version = re.search( 8 | r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE 9 | ).group( # type:ignore 10 | 1 11 | ) 12 | with open("README.md", "r", encoding="utf-8") as f: 13 | long_description = f.read() 14 | 15 | setup( 16 | name="flask-seek", 17 | python_requires=">=3.6", 18 | version=version, 19 | description="An flask extension to make your code more elegant", 20 | long_description=long_description, 21 | long_description_content_type="text/markdown", 22 | author="shangsky", 23 | author_email="t_c_y@outlook.com", 24 | maintainer="shangsky", 25 | maintainer_email="t_c_y@outlook.com", 26 | license="MIT", 27 | packages=find_packages(), 28 | platforms=["all"], 29 | url="https://github.com/ShangSky/flask-seek", 30 | classifiers=[ 31 | "Development Status :: 4 - Beta", 32 | "Operating System :: OS Independent", 33 | "Intended Audience :: Developers", 34 | "License :: OSI Approved :: MIT License", 35 | "Programming Language :: Python :: 3.6", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Topic :: Software Development :: Libraries", 40 | ], 41 | install_requires=["flask>=1.1.0"], 42 | ) 43 | --------------------------------------------------------------------------------