1 | #! /usr/bin/env python |
---|
2 | # Copyright (C) 2008-2018, Stefan Schwarzer <sschwarzer@sschwarzer.net> |
---|
3 | # and ftputil contributors (see `doc/contributors.txt`) |
---|
4 | # See the file LICENSE for licensing terms. |
---|
5 | |
---|
6 | # pylint: disable=redefined-builtin |
---|
7 | |
---|
8 | """\ |
---|
9 | This script scans a directory tree for files which contain code which |
---|
10 | is deprecated or invalid in ftputil %s and above (and even much |
---|
11 | longer). The script uses simple heuristics, so it may miss occurrences |
---|
12 | of deprecated/invalid usage or print some inappropriate lines of your |
---|
13 | files. |
---|
14 | |
---|
15 | Usage: %s start_dir |
---|
16 | |
---|
17 | where 'start_dir' is the starting directory which will be scanned |
---|
18 | recursively for offending code. |
---|
19 | """ |
---|
20 | |
---|
21 | import os |
---|
22 | import re |
---|
23 | import sys |
---|
24 | |
---|
25 | import ftputil.version |
---|
26 | |
---|
27 | |
---|
28 | __doc__ = __doc__ % (ftputil.version.__version__, |
---|
29 | os.path.basename(sys.argv[0])) |
---|
30 | |
---|
31 | |
---|
32 | class InvalidFeature: |
---|
33 | """ |
---|
34 | Store message, regex and locations of a single now invalid |
---|
35 | feature. |
---|
36 | """ |
---|
37 | |
---|
38 | # pylint: disable=too-few-public-methods |
---|
39 | def __init__(self, message, regex): |
---|
40 | self.message = message |
---|
41 | if not isinstance(regex, re.compile("").__class__): |
---|
42 | regex = re.compile(regex) |
---|
43 | self.regex = regex |
---|
44 | # Map file name to a list of line numbers (starting at 1). |
---|
45 | self.locations = {} |
---|
46 | |
---|
47 | |
---|
48 | HOST_REGEX = r"\b(h|host|ftp|ftphost|ftp_host)\b" |
---|
49 | |
---|
50 | invalid_features = [ |
---|
51 | InvalidFeature("Possible use(s) of FTP exceptions via ftputil module", |
---|
52 | r"\bftputil\s*?\.\s*?[A-Za-z]+Error\b"), |
---|
53 | InvalidFeature("Possible use(s) of ftp_error module", |
---|
54 | r"\bftp_error\b"), |
---|
55 | InvalidFeature("Possible use(s) of ftp_stat module", |
---|
56 | r"\bftp_stat\b"), |
---|
57 | InvalidFeature("Possible use(s) of FTPHost.file", |
---|
58 | r"{0}\.file\(".format(HOST_REGEX)), |
---|
59 | InvalidFeature("Possible use(s) of FTPHost.open with text mode", |
---|
60 | r"{0}\.open\(.*[\"'](r|w)t?[\"']".format(HOST_REGEX)), |
---|
61 | InvalidFeature("Possible use(s) of byte string in ignores_line", |
---|
62 | r"\bdef ignores_line\("), |
---|
63 | InvalidFeature("Possible use(s) of byte string in parse_line", |
---|
64 | r"\bdef parse_line\("), |
---|
65 | InvalidFeature("Possible use(s) download with text mode", |
---|
66 | r"{0}\.download(_if_newer)?\(".format(HOST_REGEX)), |
---|
67 | InvalidFeature("Possible use(s) upload with text mode", |
---|
68 | r"{0}\.upload(_if_newer)?\(".format(HOST_REGEX)), |
---|
69 | InvalidFeature("Possible use(s) of xreadline method of FTP file objects", |
---|
70 | r"\.\s*?xreadlines\b"), |
---|
71 | ] |
---|
72 | |
---|
73 | |
---|
74 | def scan_file(file_name): |
---|
75 | """ |
---|
76 | Scan a file with name `file_name` for code deprecated in |
---|
77 | ftputil usage and collect the offending data in the dictionary |
---|
78 | `features.locations`. |
---|
79 | """ |
---|
80 | with open(file_name) as fobj: |
---|
81 | for index, line in enumerate(fobj, start=1): |
---|
82 | for feature in invalid_features: |
---|
83 | if feature.regex.search(line): |
---|
84 | locations = feature.locations |
---|
85 | locations.setdefault(file_name, []) |
---|
86 | locations[file_name].append((index, line.rstrip())) |
---|
87 | |
---|
88 | |
---|
89 | def print_results(): |
---|
90 | """ |
---|
91 | Print statistics of deprecated code after the directory has been |
---|
92 | scanned. |
---|
93 | """ |
---|
94 | last_message = "" |
---|
95 | for feature in invalid_features: |
---|
96 | if feature.message != last_message: |
---|
97 | print() |
---|
98 | print(70 * "-") |
---|
99 | print(feature.message, "...") |
---|
100 | print() |
---|
101 | last_message = feature.message |
---|
102 | locations = feature.locations |
---|
103 | if not locations: |
---|
104 | print(" no invalid code found") |
---|
105 | continue |
---|
106 | for file_name in sorted(locations.keys()): |
---|
107 | print(file_name) |
---|
108 | for line_number, line in locations[file_name]: |
---|
109 | print("%5d: %s" % (line_number, line)) |
---|
110 | print() |
---|
111 | print("===========================================") |
---|
112 | print("Please check your code also by other means!") |
---|
113 | print("===========================================") |
---|
114 | |
---|
115 | |
---|
116 | def main(start_dir): |
---|
117 | """ |
---|
118 | Scan a directory tree starting at `start_dir` and print uses |
---|
119 | of deprecated features, if any were found. |
---|
120 | """ |
---|
121 | # Deliberately shadow global `start_dir`. |
---|
122 | # pylint: disable=redefined-outer-name |
---|
123 | for dir_path, _dirnames, file_names in os.walk(start_dir): |
---|
124 | for file_name in file_names: |
---|
125 | abs_name = os.path.abspath(os.path.join(dir_path, file_name)) |
---|
126 | if file_name.endswith(".py"): |
---|
127 | scan_file(abs_name) |
---|
128 | print_results() |
---|
129 | |
---|
130 | |
---|
131 | if __name__ == '__main__': |
---|
132 | if len(sys.argv) == 2: |
---|
133 | if sys.argv[1] in ("-h", "--help"): |
---|
134 | print(__doc__) |
---|
135 | sys.exit(0) |
---|
136 | start_dir = sys.argv[1] |
---|
137 | if not os.path.isdir(start_dir): |
---|
138 | print("Directory %s not found." % start_dir, file=sys.stderr) |
---|
139 | sys.exit() |
---|
140 | else: |
---|
141 | print("Usage: %s start_dir" % sys.argv[0], file=sys.stderr) |
---|
142 | sys.exit() |
---|
143 | main(start_dir) |
---|