Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
cli.py225 linesDownload Raw Back to watchfiles
1import argparse
2import logging
3import os
4import shlex
5import sys
6from pathlib import Path
7from textwrap import dedent
8from typing import Any, Callable, List, Optional, Tuple, Union, cast
9
10from . import Change
11from .filters import BaseFilter, DefaultFilter, PythonFilter
12from .run import detect_target_type, import_string, run_process
13from .version import VERSION
14
15logger = logging.getLogger('watchfiles.cli')
16
17
18def resolve_path(path_str: str) -> Path:
19    path = Path(path_str)
20    if not path.exists():
21        raise FileNotFoundError(path)
22    else:
23        return path.resolve()
24
25
26def cli(*args_: str) -> None:
27    """
28    Watch one or more directories and execute either a shell command or a python function on file changes.
29
30    Example of watching the current directory and calling a python function:
31
32        watchfiles foobar.main
33
34    Example of watching python files in two local directories and calling a shell command:
35
36        watchfiles --filter python 'pytest --lf' src tests
37
38    See https://watchfiles.helpmanual.io/cli/ for more information.
39    """
40    args = args_ or sys.argv[1:]
41    parser = argparse.ArgumentParser(
42        prog='watchfiles',
43        description=dedent((cli.__doc__ or '').strip('\n')),
44        formatter_class=argparse.RawTextHelpFormatter,
45    )
46    parser.add_argument('target', help='Command or dotted function path to run')
47    parser.add_argument(
48        'paths', nargs='*', default='.', help='Filesystem paths to watch, defaults to current directory'
49    )
50
51    parser.add_argument(
52        '--ignore-paths',
53        nargs='?',
54        type=str,
55        help=(
56            'Specify directories to ignore, '
57            'to ignore multiple paths use a comma as separator, e.g. "env" or "env,node_modules"'
58        ),
59    )
60    parser.add_argument(
61        '--target-type',
62        nargs='?',
63        type=str,
64        default='auto',
65        choices=['command', 'function', 'auto'],
66        help=(
67            'Whether the target should be intercepted as a shell command or a python function, '
68            'defaults to "auto" which infers the target type from the target string'
69        ),
70    )
71    parser.add_argument(
72        '--filter',
73        nargs='?',
74        type=str,
75        default='default',
76        help=(
77            'Which files to watch, defaults to "default" which uses the "DefaultFilter", '
78            '"python" uses the "PythonFilter", "all" uses no filter, '
79            'any other value is interpreted as a python function/class path which is imported'
80        ),
81    )
82    parser.add_argument(
83        '--args',
84        nargs='?',
85        type=str,
86        help='Arguments to set on sys.argv before calling target function, used only if the target is a function',
87    )
88    parser.add_argument('--verbose', action='store_true', help='Set log level to "debug", wins over `--verbosity`')
89    parser.add_argument(
90        '--non-recursive', action='store_true', help='Do not watch for changes in sub-directories recursively'
91    )
92    parser.add_argument(
93        '--verbosity',
94        nargs='?',
95        type=str,
96        default='info',
97        choices=['warning', 'info', 'debug'],
98        help='Log level, defaults to "info"',
99    )
100    parser.add_argument(
101        '--sigint-timeout',
102        nargs='?',
103        type=int,
104        default=5,
105        help='How long to wait for the sigint timeout before sending sigkill.',
106    )
107    parser.add_argument(
108        '--grace-period',
109        nargs='?',
110        type=float,
111        default=0,
112        help='Number of seconds after the process is started before watching for changes.',
113    )
114    parser.add_argument(
115        '--sigkill-timeout',
116        nargs='?',
117        type=int,
118        default=1,
119        help='How long to wait for the sigkill timeout before issuing a timeout exception.',
120    )
121    parser.add_argument(
122        '--ignore-permission-denied',
123        action='store_true',
124        help='Ignore permission denied errors while watching files and directories.',
125    )
126    parser.add_argument('--version', '-V', action='version', version=f'%(prog)s v{VERSION}')
127    arg_namespace = parser.parse_args(args)
128
129    if arg_namespace.verbose:
130        log_level = logging.DEBUG
131    else:
132        log_level = getattr(logging, arg_namespace.verbosity.upper())
133
134    hdlr = logging.StreamHandler()
135    hdlr.setLevel(log_level)
136    hdlr.setFormatter(logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%H:%M:%S'))
137    wg_logger = logging.getLogger('watchfiles')
138    wg_logger.addHandler(hdlr)
139    wg_logger.setLevel(log_level)
140
141    if arg_namespace.target_type == 'auto':
142        target_type = detect_target_type(arg_namespace.target)
143    else:
144        target_type = arg_namespace.target_type
145
146    if target_type == 'function':
147        logger.debug('target_type=function, attempting import of "%s"', arg_namespace.target)
148        import_exit(arg_namespace.target)
149        if arg_namespace.args:
150            sys.argv = [arg_namespace.target] + shlex.split(arg_namespace.args)
151    elif arg_namespace.args:
152        logger.warning('--args is only used when the target is a function')
153
154    try:
155        paths = [resolve_path(p) for p in arg_namespace.paths]
156    except FileNotFoundError as e:
157        print(f'path "{e}" does not exist', file=sys.stderr)
158        sys.exit(1)
159
160    watch_filter, watch_filter_str = build_filter(arg_namespace.filter, arg_namespace.ignore_paths)
161
162    logger.info(
163        'watchfiles v%s 👀  path=%s target="%s" (%s) filter=%s...',
164        VERSION,
165        ', '.join(f'"{p}"' for p in paths),
166        arg_namespace.target,
167        target_type,
168        watch_filter_str,
169    )
170
171    run_process(
172        *paths,
173        target=arg_namespace.target,
174        target_type=target_type,
175        watch_filter=watch_filter,
176        debug=log_level == logging.DEBUG,
177        sigint_timeout=arg_namespace.sigint_timeout,
178        sigkill_timeout=arg_namespace.sigkill_timeout,
179        recursive=not arg_namespace.non_recursive,
180        ignore_permission_denied=arg_namespace.ignore_permission_denied,
181        grace_period=arg_namespace.grace_period,
182    )
183
184
185def import_exit(function_path: str) -> Any:
186    cwd = os.getcwd()
187    if cwd not in sys.path:
188        sys.path.append(cwd)
189
190    try:
191        return import_string(function_path)
192    except ImportError as e:
193        print(f'ImportError: {e}', file=sys.stderr)
194        sys.exit(1)
195
196
197def build_filter(
198    filter_name: str, ignore_paths_str: Optional[str]
199) -> Tuple[Union[None, DefaultFilter, Callable[[Change, str], bool]], str]:
200    ignore_paths: List[Path] = []
201    if ignore_paths_str:
202        ignore_paths = [Path(p).resolve() for p in ignore_paths_str.split(',')]
203
204    if filter_name == 'default':
205        return DefaultFilter(ignore_paths=ignore_paths), 'DefaultFilter'
206    elif filter_name == 'python':
207        return PythonFilter(ignore_paths=ignore_paths), 'PythonFilter'
208    elif filter_name == 'all':
209        if ignore_paths:
210            logger.warning('"--ignore-paths" argument ignored as "all" filter was selected')
211        return None, '(no filter)'
212
213    watch_filter_cls = import_exit(filter_name)
214    if isinstance(watch_filter_cls, type) and issubclass(watch_filter_cls, DefaultFilter):
215        return watch_filter_cls(ignore_paths=ignore_paths), watch_filter_cls.__name__
216
217    if ignore_paths:
218        logger.warning('"--ignore-paths" argument ignored as filter is not a subclass of DefaultFilter')
219
220    if isinstance(watch_filter_cls, type) and issubclass(watch_filter_cls, BaseFilter):
221        return watch_filter_cls(), watch_filter_cls.__name__
222    else:
223        watch_filter = cast(Callable[[Change, str], bool], watch_filter_cls)
224        return watch_filter, repr(watch_filter_cls)
225 
codekingpro/portable-devtools · Team Ai