1111# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212# See the License for the specific language governing permissions and
1313# limitations under the License.
14- """
15- :mod:`refex.cli`
16- ================
14+ """:mod:`refex.cli` ================
1715
1816Command-line interface to Refex, and extension points to that interface.
1917
2624from __future__ import division
2725from __future__ import print_function
2826
27+ import abc
2928import argparse
3029import atexit
3130import collections
4039import tempfile
4140import textwrap
4241import traceback
43- from typing import Dict , Iterable , List , Optional , Text , Tuple , Union
42+ from typing import Dict , Generic , Iterable , Optional , Text , Tuple , TypeVar , Union , IO
4443
4544from absl import app
4645import attr
4746import colorama
4847import pkg_resources
49- import six
50-
5148from refex import formatting
5249from refex import search
5350from refex .fix import find_fixer
5451from refex .python import syntactic_template
52+ import six
5553
5654_IGNORABLE_ERRNO = frozenset ([
5755 errno .ENOENT , # file was removed after we went looking
@@ -68,6 +66,41 @@ def _shorten_path(path):
6866# given but iteration is to done.
6967_DEFAULT_ITERATION_COUNT = 10
7068
69+ MetaT = TypeVar ('MetaT' )
70+
71+
72+ @attr .s
73+ class Content (Generic [MetaT ]):
74+ data : str = attr .ib ()
75+ metadata : MetaT = attr .ib (default = None )
76+
77+
78+ class Codec (abc .ABC ):
79+ """File codec base."""
80+
81+ @abc .abstractmethod
82+ def read (self , f : IO [bytes ]) -> Content :
83+ pass
84+
85+ @abc .abstractmethod
86+ def write (self , f : IO [bytes ], content : Content ) -> None :
87+ pass
88+
89+
90+ @attr .s
91+ class UnicodeCodec (Codec ):
92+ """Standard unicode encoded content."""
93+
94+ encoding = attr .ib (default = 'utf-8' )
95+
96+ def read (self , f : IO [bytes ]) -> Content :
97+ with io .TextIOWrapper (f , self .encoding ) as f :
98+ return Content (data = f .read ())
99+
100+ def write (self , f : IO [bytes ], content : Content ) -> None :
101+ with io .TextIOWrapper (f , self .encoding ) as f :
102+ f .write (content .data )
103+
71104
72105@attr .s
73106class RefexRunner (object ):
@@ -97,8 +130,9 @@ class RefexRunner(object):
97130 show_files = attr .ib (default = True )
98131 verbose = attr .ib (default = False )
99132 max_iterations = attr .ib (default = _DEFAULT_ITERATION_COUNT )
133+ codec = attr .ib (default = UnicodeCodec ())
100134
101- def read (self , path : str ) -> Optional [Text ]:
135+ def read (self , path : str ) -> Optional [Content ]:
102136 """Reads in a file and return the resulting content as unicode.
103137
104138 Since this is only called from the loop within :meth:`rewrite_files`,
@@ -108,11 +142,11 @@ def read(self, path: str) -> Optional[Text]:
108142 path: The path to the file.
109143
110144 Returns:
111- An optional unicode string of the file content .
145+ An optional TransformationResult .
112146 """
113147 try :
114- with io .open (path , 'r' , encoding = 'utf-8 ' ) as d :
115- return d . read ()
148+ with io .open (path , 'rb ' ) as d :
149+ return self . codec . read (d )
116150 except UnicodeDecodeError as e :
117151 print ('skipped %s: UnicodeDecodeError: %s' % (path , e ), file = sys .stderr )
118152 return None
@@ -142,11 +176,12 @@ def get_matches(self, contents, path):
142176 print ('skipped %s: %s' % (path , e ), file = sys .stderr )
143177 return []
144178
145- def write (self , path , content , matches ):
179+ def write (self , path , result , matches ):
146180 if not self .dry_run :
147181 try :
148- with io .open (path , 'w' , encoding = 'utf-8' ) as f :
149- f .write (formatting .apply_substitutions (content , matches ))
182+ with io .open (path , 'wb' ) as f :
183+ result .data = formatting .apply_substitutions (result .data , matches )
184+ self .codec .write (f , result )
150185 except IOError as e :
151186 print ('skipped %s: IOError: %s' % (path , e ), file = sys .stderr )
152187
@@ -175,7 +210,7 @@ def log_changes(self, content, matches, name, renderer):
175210 if part :
176211 sys .stdout .write (part )
177212 sys .stdout .flush ()
178- return has_changes
213+ return has_any_changes
179214
180215 def rewrite_files (self , path_pairs ):
181216 """Main access point for rewriting.
@@ -193,13 +228,13 @@ def rewrite_files(self, path_pairs):
193228 has_changes = False
194229 for read , write in path_pairs :
195230 display_name = _shorten_path (write )
196- content = self .read (read )
197- if content is not None :
231+ result = self .read (read )
232+ if result is not None :
198233 try :
199- matches = self .get_matches (content , display_name )
234+ matches = self .get_matches (result . data , display_name )
200235 except Exception as e : # pylint: disable=broad-except
201236 failures [read ] = {
202- 'content' : content ,
237+ 'content' : result . data ,
203238 'traceback' : traceback .format_exc ()
204239 }
205240 print (
@@ -208,8 +243,9 @@ def rewrite_files(self, path_pairs):
208243 file = sys .stderr )
209244 else :
210245 has_changes |= (
211- self .log_changes (content , matches , display_name , self .renderer ))
212- self .write (write , content , matches )
246+ self .log_changes (result .data , matches , display_name ,
247+ self .renderer ))
248+ self .write (write , result , matches )
213249 if has_changes and self .dry_run :
214250 # If there were changes that the user might have wanted to apply, but they
215251 # were in dry run mode, print a note for them.
@@ -219,7 +255,6 @@ def rewrite_files(self, path_pairs):
219255
220256_BUG_REPORT_URL = 'https://github.com/ssbr/refex/issues/new/choose'
221257
222-
223258# It was at this point, dear reader, that this programmer wondered if using
224259# argparse was a mistake after all.
225260#
@@ -358,13 +393,12 @@ def run_cli(argv,
358393 Args:
359394 argv: argv
360395 parser: An ArgumentParser.
361- get_runner: called with (parser, options)
362- returns the runner to use.
363- get_files: called with (runner, options)
364- returns the files to examine, as [(in_file, out_file), ...] pairs.
365- bug_report_url: An URL to present to the user to report bugs.
366- As the error dump includes source code, corporate organizations may
367- wish to override this with an internal bug report link for triage.
396+ get_runner: called with (parser, options) returns the runner to use.
397+ get_files: called with (runner, options) returns the files to examine, as
398+ [(in_file, out_file), ...] pairs.
399+ bug_report_url: An URL to present to the user to report bugs. As the error
400+ dump includes source code, corporate organizations may wish to override
401+ this with an internal bug report link for triage.
368402 version: The version number to use in bug report logs and --version
369403 """
370404 with _report_bug_excepthook (bug_report_url ):
@@ -547,15 +581,17 @@ def _add_rewriter_arguments(parser):
547581 help = 'Expand passed file paths recursively.' )
548582 parser .add_argument ('--norecursive' , action = 'store_false' , dest = 'recursive' )
549583
550- parser .add_argument ('--excludefile' ,
551- type = re .compile ,
552- metavar = 'REGEX' ,
553- help = 'Filenames to exclude (regular expression).' )
554- parser .add_argument ('--includefile' ,
555- type = re .compile ,
556- metavar = 'REGEX' ,
557- help = 'Filenames that must match to include'
558- ' (regular expression).' )
584+ parser .add_argument (
585+ '--excludefile' ,
586+ type = re .compile ,
587+ metavar = 'REGEX' ,
588+ help = 'Filenames to exclude (regular expression).' )
589+ parser .add_argument (
590+ '--includefile' ,
591+ type = re .compile ,
592+ metavar = 'REGEX' ,
593+ help = 'Filenames that must match to include'
594+ ' (regular expression).' )
559595 parser .add_argument (
560596 '--also' ,
561597 type = search .default_compile_regex ,
@@ -619,20 +655,22 @@ def _add_rewriter_arguments(parser):
619655 action = 'store_true' ,
620656 dest = 'print_filename' ,
621657 help = 'Print the filename in output'
622- ' (true by default, but disabled by --no-filename).' ,)
658+ ' (true by default, but disabled by --no-filename).' ,
659+ )
623660 dry_run_arguments = parser .add_mutually_exclusive_group ()
624661 dry_run_arguments .add_argument (
625662 '--dry-run' ,
626663 action = 'store_const' ,
627664 const = False ,
628665 dest = 'in_place' ,
629666 help = "Don't write anything to disk. (The default)" )
630- dry_run_arguments .add_argument ('--in-place' ,
631- '-i' ,
632- action = 'store_const' ,
633- const = True ,
634- dest = 'in_place' ,
635- help = 'Write changes back to disk.' )
667+ dry_run_arguments .add_argument (
668+ '--in-place' ,
669+ '-i' ,
670+ action = 'store_const' ,
671+ const = True ,
672+ dest = 'in_place' ,
673+ help = 'Write changes back to disk.' )
636674
637675 debug_options .add_argument (
638676 '--profile-to' ,
@@ -708,8 +746,8 @@ def _parse_options(argv, parser):
708746 options , args = _parse_args_leftovers (parser , argv )
709747 options .files = []
710748 if options .pattern_or_file is not None :
711- if (len (options .search_replace ) == 1
712- and options .search_replace [0 ].match is None ):
749+ if (len (options .search_replace ) == 1 and
750+ options .search_replace [0 ].match is None ):
713751 options .search_replace [0 ].match = options .pattern_or_file
714752 else :
715753 options .files .append (options .pattern_or_file )
@@ -831,9 +869,7 @@ def argument_parser(version):
831869 )
832870
833871 parser .set_defaults (
834- rewriter = None ,
835- ** {search_replace_dest : [_SearchReplaceArgument ()]}
836- )
872+ rewriter = None , ** {search_replace_dest : [_SearchReplaceArgument ()]})
837873
838874 return parser
839875
0 commit comments