1
- import sys
1
+ from __future__ import annotations
2
2
import os
3
- import click
3
+ from regendoc .actions import Action
4
+ from typing import Callable , Generator , Sequence
5
+ import contextlib
6
+ import typer
4
7
import tempfile
5
- import re
6
- import io
7
-
8
+ from pathlib import Path
8
9
from .parse import parse_actions , correct_content
9
- from .actions import ACTIONS
10
+ from .substitute import SubstituteAddress , SubstituteRegex , default_substituters
11
+ import logging
12
+ from rich .logging import RichHandler
13
+ from rich .console import Console
14
+ from rich .progress import BarColumn , Progress , TextColumn , TimeRemainingColumn
15
+
16
+ log = logging .getLogger ("regendoc" )
17
+ log .propagate = False
10
18
19
+ NORMALIZERS = Sequence [Callable [[str ], str ]]
11
20
12
- def normalize_content (content , operators ):
21
+
22
+ def normalize_content (content : str , operators : NORMALIZERS ) -> str :
13
23
lines = content .splitlines (True )
14
24
result = []
15
25
for line in lines :
@@ -19,21 +29,23 @@ def normalize_content(content, operators):
19
29
return "" .join (result )
20
30
21
31
22
- def check_file (name , content , tmp_dir , normalize , verbose = True ):
32
+ def check_file (
33
+ path : Path , content : list [str ], tmp_dir : Path , normalize : NORMALIZERS
34
+ ) -> list [Action ]:
23
35
needed_updates = []
24
- for action in parse_actions (content , file = name ):
25
- method = ACTIONS [action ["action" ]]
26
- new_content = method (
27
- name = name , target_dir = tmp_dir , action = action , verbose = verbose
28
- )
36
+ for action in parse_actions (content , file = path ):
37
+
38
+ new_content = action (tmp_dir )
29
39
if new_content :
30
- action [ " new_content" ] = normalize_content (new_content , normalize )
40
+ action . new_content = normalize_content (new_content , normalize )
31
41
needed_updates .append (action )
32
42
return needed_updates
33
43
34
44
35
- def print_diff (action ):
36
- content , out = action ["content" ], action ["new_content" ]
45
+ def print_diff (action : Action , console : Console ) -> None :
46
+ content , out = action .content , action .new_content
47
+ assert out is not None
48
+
37
49
if out != content :
38
50
import difflib
39
51
@@ -42,74 +54,88 @@ def print_diff(action):
42
54
contl = content .splitlines (True )
43
55
lines = differ .compare (contl , outl )
44
56
45
- mapping = {"+" : "green" , "-" : "red" , "?" : "blue" }
57
+ styles = {"+" : "bold green" , "-" : "bold red" , "?" : "bold blue" }
46
58
if lines :
47
- click . secho ("$ " + action [ " target" ], bold = True , fg = " blue" )
59
+ console . print ("$" , action . target , style = "bold blue" )
48
60
for line in lines :
49
- color = mapping .get (line [0 ])
50
- if color :
51
- click .secho (line , fg = color , bold = True , nl = False )
52
- else :
53
- click .echo (line , nl = False )
61
+ style = styles .get (line [0 ])
62
+ console .print (line , style = style , end = "" )
54
63
55
64
56
- class Substituter (object ):
57
- def __init__ (self , match , replace ):
58
- self .match = match
59
- self .replace = replace
65
+ def mktemp (rootdir : Path | None , name : str ) -> Path :
66
+ if rootdir is not None :
67
+ return Path (rootdir )
68
+ root = tempfile .gettempdir ()
69
+ rootdir = Path (root , "regendoc-tmpdirs" )
70
+ rootdir .mkdir (exist_ok = True )
71
+ return Path (tempfile .mkdtemp (prefix = name + "-" , dir = rootdir ))
60
72
61
- @classmethod
62
- def parse (cls , s ):
63
- parts = s .split (s [0 ])
64
- assert len (parts ) == 4
65
- return cls (match = parts [1 ], replace = parts [2 ])
66
73
67
- def __call__ ( self , line ):
68
- return re . sub ( self . match , self . replace , line )
74
+ @ contextlib . contextmanager
75
+ def ux_setup ( verbose : bool ) -> Generator [ Progress , None , None ]:
69
76
70
- def __repr__ (self ):
71
- return "<Substituter {self.match!r} to {self.replace!r}>" .format (self = self )
77
+ columns = [
78
+ TextColumn ("[progress.description]{task.description}" ),
79
+ TextColumn ("[progress.percentage]{task.completed:3.0f}/{task.total:3.0f}" ),
80
+ BarColumn (),
81
+ TextColumn ("[progress.percentage]{task.percentage:>3.0f}%" ),
82
+ TimeRemainingColumn (),
83
+ ]
84
+ with Progress (
85
+ * columns ,
86
+ ) as progress :
87
+
88
+ handler = RichHandler (
89
+ markup = True ,
90
+ rich_tracebacks = True ,
91
+ show_path = False ,
92
+ log_time_format = "[%H:%m]" ,
93
+ console = progress .console ,
94
+ )
95
+ log .addHandler (handler )
96
+ log .setLevel (logging .DEBUG if verbose else logging .INFO )
72
97
98
+ yield progress
73
99
74
- def default_substituters (targetdir ):
75
- return [
76
- Substituter (match = re .escape (targetdir ), replace = "/path/to/example" ),
77
- Substituter (match = re .escape (os .getcwd ()), replace = "$PWD" ),
78
- Substituter (match = r"at 0x[0-9a-f]+>" , replace = "at 0xdeadbeef>" ),
79
- Substituter (match = re .escape (sys .prefix ), replace = '$PYTHON_PREFIX' ),
80
- ]
81
100
101
+ def _main (
102
+ files : list [Path ],
103
+ update : bool = typer .Option (False , "--update" ),
104
+ normalize : list [str ] = typer .Option (default = []),
105
+ rootdir : Path | None = None ,
106
+ def_name : str | None = None ,
107
+ verbose : bool = typer .Option (False , "--verbose" ),
108
+ ) -> None :
82
109
83
- @click .command ()
84
- @click .argument ("files" , nargs = - 1 )
85
- @click .option ("--update" , is_flag = True )
86
- @click .option ("--normalize" , type = Substituter .parse , multiple = True )
87
- @click .option ("--verbose" , default = False , is_flag = True )
88
- def main (files , update , normalize = (), rootdir = None , verbose = False ):
89
- tmpdir = rootdir or tempfile .mkdtemp (prefix = "regendoc-exec-" )
90
- total = len (files )
91
- for num , name in enumerate (files , 1 ):
92
- targetdir = os .path .join (tmpdir , "%s-%d" % (os .path .basename (name ), num ))
93
- with io .open (name , encoding = "UTF-8" ) as fp :
94
- content = list (fp )
95
- os .mkdir (targetdir )
96
- click .secho (
97
- "#[{num:3d}/{total:3d}] {name}" .format (num = num , total = total , name = name ),
98
- bold = True ,
99
- )
100
- updates = check_file (
101
- name = name ,
102
- content = content ,
103
- tmp_dir = targetdir ,
104
- normalize = default_substituters (targetdir ) + list (normalize ),
105
- verbose = verbose ,
106
- )
107
- for action in updates :
108
- if action ["content" ] is None or action ["new_content" ] is None :
109
- continue
110
-
111
- print_diff (action )
112
- if update :
113
- corrected = correct_content (content , updates )
114
- with io .open (name , "w" , encoding = "UTF-8" ) as f :
115
- f .writelines (corrected )
110
+ parsed_normalize : list [SubstituteRegex | SubstituteAddress ] = [
111
+ SubstituteRegex .parse (s ) for s in normalize
112
+ ]
113
+
114
+ cwd = Path .cwd ()
115
+ tmpdir : Path = mktemp (rootdir , cwd .name )
116
+
117
+ with ux_setup (verbose ) as progress :
118
+ task_id = progress .add_task (description = "progressing files" )
119
+ for num , name in enumerate (progress .track (files , task_id = task_id )):
120
+
121
+ targetdir = tmpdir .joinpath ("%s-%d" % (os .path .basename (name ), num ))
122
+ with open (name ) as fp :
123
+ content = list (fp )
124
+ targetdir .mkdir ()
125
+ log .info (f"[bold]{ name } [/bold]" )
126
+ updates = check_file (
127
+ path = name ,
128
+ content = content ,
129
+ tmp_dir = targetdir ,
130
+ normalize = default_substituters (targetdir ) + parsed_normalize ,
131
+ )
132
+ for action in updates :
133
+ print_diff (action , progress .console )
134
+ if update :
135
+ corrected = correct_content (content , updates )
136
+ with open (name , "w" ) as f :
137
+ f .writelines (corrected )
138
+
139
+
140
+ def main () -> None :
141
+ typer .run (_main )
0 commit comments