Make running individual Python tests less painful

Before this change, running Python tests individually required
building a tox environment via the run_tests script and then specifying
long environment variables to filter out just the test we wanted to run
(and then we wouldn't be able to get the output on interrupt, nor would
we have an easy way of determining the PID of the process for debugger
attachment). Now invoking the build_python.sh script creates a workable
python virtual environment that includes all necessary libraries and
tests (s.t. running a single test is now possible by just knowing the
module name). This does not change existing supported means of running
tests (e.g. through run_tests.py).

An additional way of running individual tests has been introduced.
Following invocation of `./tools/run_tests/build_python.sh` (or
run_tests.py), one may invoke

  ./$VENV/bin/python -m $TEST_MODULE_NAME

and acquire a single running process that *is* the test process (rather
than a parent of the process). $VENV is the virtual environment name
specified to `build_python.sh` (defaults to `py27`) and
$TEST_MODULE_NAME is what it says on the tin.
diff --git a/tools/run_tests/run_tests.py b/tools/run_tests/run_tests.py
index f32a621..fbc5729 100755
--- a/tools/run_tests/run_tests.py
+++ b/tools/run_tests/run_tests.py
@@ -32,11 +32,13 @@
 
 import argparse
 import ast
+import collections
 import glob
 import itertools
 import json
 import multiprocessing
 import os
+import os.path
 import platform
 import random
 import re
@@ -372,12 +374,20 @@
     return 'php'
 
 
+class PythonConfig(collections.namedtuple('PythonConfig', [
+    'python', 'venv', 'venv_relative_python', 'toolchain',])):
+
+  @property
+  def venv_python(self):
+    return os.path.abspath('{}/{}'.format(self.venv, self.venv_relative_python))
+
+
 class PythonLanguage(object):
 
   def configure(self, config, args):
     self.config = config
     self.args = args
-    self._tox_envs = self._get_tox_envs(self.args.compiler)
+    self.pythons = self._get_pythons(self.args.compiler)
 
   def test_specs(self):
     # load list of known test suites
@@ -386,33 +396,42 @@
     environment = dict(_FORCE_ENVIRON_FOR_WRAPPERS)
     if self.config.build_config != 'gcov':
       return [self.config.job_spec(
-          ['tools/run_tests/run_python.sh', tox_env],
+          ['tools/run_tests/run_python.sh', config.venv_python],
+          None,
           environ=dict(environment.items() +
                        [('GRPC_PYTHON_TESTRUNNER_FILTER', suite_name)]),
-          shortname='%s.test.%s' % (tox_env, suite_name),
+          shortname='%s.test.%s' % (config.venv, suite_name),
           timeout_seconds=5*60)
           for suite_name in tests_json
-          for tox_env in self._tox_envs]
+          for config in self.pythons]
     else:
-      return [self.config.job_spec(['tools/run_tests/run_python.sh', tox_env],
-                                   environ=environment,
-                                   shortname='%s.test.coverage' % tox_env,
-                                   timeout_seconds=15*60)
-                                   for tox_env in self._tox_envs]
+      return [self.config.job_spec(
+          ['tools/run_tests/run_python.sh', config.venv_python],
+          None,
+          environ=environment,
+          shortname='%s.test.coverage' % config.venv,
+          timeout_seconds=15*60)
+          for config in self.pythons]
 
 
   def pre_build_steps(self):
     return []
 
   def make_targets(self):
-    return ['static_c', 'grpc_python_plugin', 'shared_c']
+    return []
 
   def make_options(self):
     return []
 
   def build_steps(self):
-    return [['tools/run_tests/build_python.sh', tox_env]
-            for tox_env in self._tox_envs]
+    return [
+        [
+            'tools/run_tests/build_python.sh',
+            config.python, config.venv,
+            config.venv_relative_python, config.toolchain
+        ]
+        for config in self.pythons
+    ]
 
   def post_tests_steps(self):
     return []
@@ -423,14 +442,21 @@
   def dockerfile_dir(self):
     return 'tools/dockerfile/test/python_jessie_%s' % _docker_arch_suffix(self.args.arch)
 
-  def _get_tox_envs(self, compiler):
-    """Returns name of tox environment based on selected compiler."""
+  def _get_pythons(self, compiler):
+    if os.name == 'nt':
+      venv_relative_python = 'Scripts/python.exe'
+      toolchain = 'mingw32'
+    else:
+      venv_relative_python = 'bin/python'
+      toolchain = 'unix'
+    python27_config = PythonConfig('python2.7', 'py27', venv_relative_python, toolchain)
+    python34_config = PythonConfig('python3.4', 'py34', venv_relative_python, toolchain)
     if compiler == 'default':
-      return ('py27', 'py34')
+      return (python27_config, python34_config,)
     elif compiler == 'python2.7':
-      return ('py27',)
+      return (python27_config,)
     elif compiler == 'python3.4':
-      return ('py34',)
+      return (python34_config,)
     else:
       raise Exception('Compiler %s not supported.' % compiler)