from __future__ import print_function

import sys, os, io, re
from twisted.internet import reactor, protocol, task, defer
from twisted.python.procutils import which
from twisted.python import usage

# run the command with python's deprecation warnings turned on, capturing
# stderr. When done, scan stderr for warnings, write them to a separate
# logfile (so the buildbot can see them), and return rc=1 if there were any.

class Options(usage.Options):
    optParameters = [
        ["warnings", None, None, "file to write warnings into at end of test run"],
        ["package", None, None, "Python package to which to restrict warning collection"]
        ]

    def parseArgs(self, command, *args):
        self["command"] = command
        self["args"] = list(args)

    description = """Run as:
PYTHONWARNINGS=default::DeprecationWarning python run-deprecations.py [--warnings=STDERRFILE] [--package=PYTHONPACKAGE ] COMMAND ARGS..
"""

class RunPP(protocol.ProcessProtocol):
    def outReceived(self, data):
        self.stdout.write(data)
        sys.stdout.write(data)
    def errReceived(self, data):
        self.stderr.write(data)
        sys.stderr.write(data)
    def processEnded(self, reason):
        signal = reason.value.signal
        rc = reason.value.exitCode
        self.d.callback((signal, rc))


def make_matcher(options):
    """
    Make a function that matches a line with a relevant deprecation.

    A deprecation warning line looks something like this::

      somepath/foo/bar/baz.py:43: DeprecationWarning: Foo is deprecated, try bar instead.

    Sadly there is no guarantee warnings begin at the beginning of a line
    since they are written to output without coordination with whatever other
    Python code is running in the process.

    :return: A one-argument callable that accepts a string and returns
        ``True`` if it contains an interesting warning and ``False``
        otherwise.
    """
    pattern = r".*\.py[oc]?:\d+:" # (Pending)?DeprecationWarning: .*"
    if options["package"]:
        pattern = r".*/{}/".format(
            re.escape(options["package"]),
        ) + pattern
    expression = re.compile(pattern)
    def match(line):
        return expression.match(line) is not None
    return match


@defer.inlineCallbacks
def run_command(main):
    config = Options()
    config.parseOptions()

    command = config["command"]
    if "/" in command:
        # don't search
        exe = command
    else:
        executables = which(command)
        if not executables:
            raise ValueError("unable to find '%s' in PATH (%s)" %
                             (command, os.environ.get("PATH")))
        exe = executables[0]

    pw = os.environ.get("PYTHONWARNINGS")
    DDW = "default::DeprecationWarning"
    if pw != DDW:
        print("note: $PYTHONWARNINGS is '%s', not the expected %s" % (pw, DDW))
        sys.stdout.flush()

    pp = RunPP()
    pp.d = defer.Deferred()
    pp.stdout = io.BytesIO()
    pp.stderr = io.BytesIO()
    reactor.spawnProcess(pp, exe, [exe] + config["args"], env=None)
    (signal, rc) = yield pp.d

    match = make_matcher(config)

    # maintain ordering, but ignore duplicates (for some reason, either the
    # 'warnings' module or twisted.python.deprecate isn't quashing them)
    already = set()
    warnings = []
    def add(line):
        if line in already:
            return
        already.add(line)
        warnings.append(line)

    pp.stdout.seek(0)
    for line in pp.stdout.readlines():
        if match(line):
            add(line) # includes newline

    pp.stderr.seek(0)
    for line in pp.stderr.readlines():
        if match(line):
            add(line)

    if warnings:
        if config["warnings"]:
            with open(config["warnings"], "wb") as f:
                print("".join(warnings), file=f)
        print("ERROR: %d deprecation warnings found" % len(warnings))
        sys.exit(1)

    print("no deprecation warnings")
    if signal:
        sys.exit(signal)
    sys.exit(rc)


task.react(run_command)
