Skip to content

Commit b796853

Browse files
Anonymous Googlercopybara-github
authored andcommitted
Add visibility of refex to tool for refactoring jupyter notebooks.
PiperOrigin-RevId: 474587955
1 parent c5768b8 commit b796853

File tree

1 file changed

+85
-49
lines changed

1 file changed

+85
-49
lines changed

refex/cli.py

Lines changed: 85 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
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
1816
Command-line interface to Refex, and extension points to that interface.
1917
@@ -26,6 +24,7 @@
2624
from __future__ import division
2725
from __future__ import print_function
2826

27+
import abc
2928
import argparse
3029
import atexit
3130
import collections
@@ -40,18 +39,17 @@
4039
import tempfile
4140
import textwrap
4241
import 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

4544
from absl import app
4645
import attr
4746
import colorama
4847
import pkg_resources
49-
import six
50-
5148
from refex import formatting
5249
from refex import search
5350
from refex.fix import find_fixer
5451
from 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
73106
class 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

Comments
 (0)