22
33from __future__ import annotations
44
5+ import importlib .util
56from pathlib import Path
67from typing import TYPE_CHECKING
8+ from typing import Iterable
79
810import click
911import typed_settings as ts
1012from click import Context
1113from pluggy import PluginManager
1214
15+ from _pytask .path import import_path
1316from _pytask .pluginmanager import hookimpl
17+ from _pytask .pluginmanager import register_hook_impls_from_modules
18+ from _pytask .pluginmanager import storage
1419
1520if TYPE_CHECKING :
1621 from _pytask .settings import SettingsBuilder
1722
1823
24+ def _hook_module_callback (
25+ ctx : Context ,
26+ name : str , # noqa: ARG001
27+ value : tuple [str , ...],
28+ ) -> Iterable [str | Path ]:
29+ """Register the user's hook modules from the configuration file."""
30+ if not value :
31+ return value
32+
33+ parsed_modules = []
34+ for module_name in value :
35+ if module_name .endswith (".py" ):
36+ path = Path (module_name )
37+ if ctx .params ["config" ]:
38+ path = ctx .params ["config" ].parent .joinpath (path ).resolve ()
39+ else :
40+ path = Path .cwd ().joinpath (path ).resolve ()
41+
42+ if not path .exists ():
43+ msg = (
44+ f"The hook module { path } does not exist. "
45+ "Please provide a valid path."
46+ )
47+ raise click .BadParameter (msg )
48+ module = import_path (path , ctx .params ["root" ])
49+ parsed_modules .append (module .__name__ )
50+ else :
51+ spec = importlib .util .find_spec (module_name )
52+ if spec is None :
53+ msg = (
54+ f"The hook module { module_name !r} is not importable. "
55+ "Please provide a valid module name."
56+ )
57+ raise click .BadParameter (msg )
58+ parsed_modules .append (module_name )
59+
60+ # If there are hook modules, we register a hook implementation to add them.
61+ # ``pytask_add_hooks`` is a historic hook specification, so even command line
62+ # options can be added.
63+ if parsed_modules :
64+
65+ class HookModule :
66+ @staticmethod
67+ @hookimpl
68+ def pytask_add_hooks (pm : PluginManager ) -> None :
69+ """Add hooks."""
70+ register_hook_impls_from_modules (pm , parsed_modules )
71+
72+ pm = storage .get ()
73+ pm .register (HookModule )
74+
75+ return parsed_modules
76+
77+
1978def _path_callback (
2079 ctx : Context , # noqa: ARG001
2180 param : click .Parameter , # noqa: ARG001
@@ -29,6 +88,49 @@ def _path_callback(
2988class Common :
3089 """Common settings for the command line interface."""
3190
91+ debug_pytask : bool = ts .option (
92+ default = False ,
93+ click = {"param_decls" : ("--debug-pytask" ,), "is_flag" : True },
94+ help = "Trace all function calls in the plugin framework." ,
95+ )
96+ editor_url_scheme : str = ts .option (
97+ default = "file" ,
98+ click = {"param_decls" : ["--editor-url-scheme" ]},
99+ help = (
100+ "Use file, vscode, pycharm or a custom url scheme to add URLs to task "
101+ "ids to quickly jump to the task definition. Use no_link to disable URLs."
102+ ),
103+ )
104+ ignore : tuple [str , ...] = ts .option (
105+ factory = tuple ,
106+ help = (
107+ "A pattern to ignore files or directories. Refer to 'pathlib.Path.match' "
108+ "for more info."
109+ ),
110+ click = {"param_decls" : ["--ignore" ], "multiple" : True },
111+ )
112+ verbose : int = ts .option (
113+ default = 1 ,
114+ help = "Make pytask verbose (>= 0) or quiet (= 0)." ,
115+ click = {
116+ "param_decls" : ["-v" , "--verbose" ],
117+ "type" : click .IntRange (0 , 2 ),
118+ "count" : True ,
119+ },
120+ )
121+ hook_module : tuple [str , ...] = ts .option (
122+ factory = list ,
123+ help = "Path to a Python module that contains hook implementations." ,
124+ click = {
125+ "param_decls" : ["--hook-module" ],
126+ "multiple" : True ,
127+ "is_eager" : True ,
128+ "callback" : _hook_module_callback ,
129+ },
130+ )
131+ config_file : Path | None = ts .option (
132+ default = None , click = {"param_decls" : ["--config-file" ], "hidden" : True }
133+ )
32134 paths : tuple [Path , ...] = ts .option (
33135 factory = tuple ,
34136 click = {
0 commit comments