1 | |
---|
2 | import sys, os, io, re |
---|
3 | from twisted.internet import reactor, protocol, task, defer |
---|
4 | from twisted.python.procutils import which |
---|
5 | from twisted.python import usage |
---|
6 | |
---|
7 | # run the command with python's deprecation warnings turned on, capturing |
---|
8 | # stderr. When done, scan stderr for warnings, write them to a separate |
---|
9 | # logfile (so the buildbot can see them), and return rc=1 if there were any. |
---|
10 | |
---|
11 | class Options(usage.Options): |
---|
12 | optParameters = [ |
---|
13 | ["warnings", None, None, "file to write warnings into at end of test run"], |
---|
14 | ["package", None, None, "Python package to which to restrict warning collection"] |
---|
15 | ] |
---|
16 | |
---|
17 | def parseArgs(self, command, *args): |
---|
18 | self["command"] = command |
---|
19 | self["args"] = list(args) |
---|
20 | |
---|
21 | description = """Run as: |
---|
22 | python run-deprecations.py [--warnings=STDERRFILE] [--package=PYTHONPACKAGE ] COMMAND ARGS.. |
---|
23 | """ |
---|
24 | |
---|
25 | class RunPP(protocol.ProcessProtocol): |
---|
26 | def outReceived(self, data): |
---|
27 | self.stdout.write(data) |
---|
28 | sys.stdout.write(str(data, sys.stdout.encoding)) |
---|
29 | def errReceived(self, data): |
---|
30 | self.stderr.write(data) |
---|
31 | sys.stderr.write(str(data, sys.stdout.encoding)) |
---|
32 | def processEnded(self, reason): |
---|
33 | signal = reason.value.signal |
---|
34 | rc = reason.value.exitCode |
---|
35 | self.d.callback((signal, rc)) |
---|
36 | |
---|
37 | |
---|
38 | def make_matcher(options): |
---|
39 | """ |
---|
40 | Make a function that matches a line with a relevant deprecation. |
---|
41 | |
---|
42 | A deprecation warning line looks something like this:: |
---|
43 | |
---|
44 | somepath/foo/bar/baz.py:43: DeprecationWarning: Foo is deprecated, try bar instead. |
---|
45 | |
---|
46 | Sadly there is no guarantee warnings begin at the beginning of a line |
---|
47 | since they are written to output without coordination with whatever other |
---|
48 | Python code is running in the process. |
---|
49 | |
---|
50 | :return: A one-argument callable that accepts a string and returns |
---|
51 | ``True`` if it contains an interesting warning and ``False`` |
---|
52 | otherwise. |
---|
53 | """ |
---|
54 | pattern = r".*\.py[oc]?:\d+:" # (Pending)?DeprecationWarning: .*" |
---|
55 | if options["package"]: |
---|
56 | pattern = r".*/{}/".format( |
---|
57 | re.escape(options["package"]), |
---|
58 | ) + pattern |
---|
59 | expression = re.compile(pattern) |
---|
60 | def match(line): |
---|
61 | return expression.match(line) is not None |
---|
62 | return match |
---|
63 | |
---|
64 | |
---|
65 | @defer.inlineCallbacks |
---|
66 | def run_command(main): |
---|
67 | config = Options() |
---|
68 | config.parseOptions() |
---|
69 | |
---|
70 | command = config["command"] |
---|
71 | if "/" in command: |
---|
72 | # don't search |
---|
73 | exe = command |
---|
74 | else: |
---|
75 | executables = which(command) |
---|
76 | if not executables: |
---|
77 | raise ValueError("unable to find '%s' in PATH (%s)" % |
---|
78 | (command, os.environ.get("PATH"))) |
---|
79 | exe = executables[0] |
---|
80 | |
---|
81 | pp = RunPP() |
---|
82 | pp.d = defer.Deferred() |
---|
83 | pp.stdout = io.BytesIO() |
---|
84 | pp.stderr = io.BytesIO() |
---|
85 | reactor.spawnProcess(pp, exe, [exe] + config["args"], env=None) |
---|
86 | (signal, rc) = yield pp.d |
---|
87 | |
---|
88 | match = make_matcher(config) |
---|
89 | |
---|
90 | # maintain ordering, but ignore duplicates (for some reason, either the |
---|
91 | # 'warnings' module or twisted.python.deprecate isn't quashing them) |
---|
92 | already = set() |
---|
93 | warnings = [] |
---|
94 | def add(line): |
---|
95 | if line in already: |
---|
96 | return |
---|
97 | already.add(line) |
---|
98 | warnings.append(line) |
---|
99 | |
---|
100 | pp.stdout.seek(0) |
---|
101 | for line in pp.stdout.readlines(): |
---|
102 | line = str(line, sys.stdout.encoding) |
---|
103 | if match(line): |
---|
104 | add(line) # includes newline |
---|
105 | |
---|
106 | pp.stderr.seek(0) |
---|
107 | for line in pp.stderr.readlines(): |
---|
108 | line = str(line, sys.stdout.encoding) |
---|
109 | if match(line): |
---|
110 | add(line) |
---|
111 | |
---|
112 | if warnings: |
---|
113 | if config["warnings"]: |
---|
114 | with open(config["warnings"], "w") as f: |
---|
115 | print("".join(warnings), file=f) |
---|
116 | print("ERROR: %d deprecation warnings found" % len(warnings)) |
---|
117 | sys.exit(1) |
---|
118 | |
---|
119 | print("no deprecation warnings") |
---|
120 | if signal: |
---|
121 | sys.exit(signal) |
---|
122 | sys.exit(rc) |
---|
123 | |
---|
124 | |
---|
125 | task.react(run_command) |
---|