1# Copyright 2015 gRPC authors. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Provides setuptools command classes for the gRPC Python setup process.""" 15 16import glob 17import os 18import os.path 19import platform 20import re 21import shutil 22import sys 23 24import setuptools 25from setuptools import errors as _errors 26from setuptools.command import build_ext 27from setuptools.command import build_py 28from setuptools.command import easy_install 29from setuptools.command import install 30from setuptools.command import test 31 32PYTHON_STEM = os.path.dirname(os.path.abspath(__file__)) 33GRPC_STEM = os.path.abspath(PYTHON_STEM + "../../../../") 34GRPC_PROTO_STEM = os.path.join(GRPC_STEM, "src", "proto") 35PROTO_STEM = os.path.join(PYTHON_STEM, "src", "proto") 36PYTHON_PROTO_TOP_LEVEL = os.path.join(PYTHON_STEM, "src") 37 38 39class CommandError(object): 40 pass 41 42 43class GatherProto(setuptools.Command): 44 description = "gather proto dependencies" 45 user_options = [] 46 47 def initialize_options(self): 48 pass 49 50 def finalize_options(self): 51 pass 52 53 def run(self): 54 # TODO(atash) ensure that we're running from the repository directory when 55 # this command is used 56 try: 57 shutil.rmtree(PROTO_STEM) 58 except Exception as error: 59 # We don't care if this command fails 60 pass 61 shutil.copytree(GRPC_PROTO_STEM, PROTO_STEM) 62 for root, _, _ in os.walk(PYTHON_PROTO_TOP_LEVEL): 63 path = os.path.join(root, "__init__.py") 64 open(path, "a").close() 65 66 67class BuildPy(build_py.build_py): 68 """Custom project build command.""" 69 70 def run(self): 71 try: 72 self.run_command("build_package_protos") 73 except CommandError as error: 74 sys.stderr.write("warning: %s\n" % error.message) 75 build_py.build_py.run(self) 76 77 78class TestLite(setuptools.Command): 79 """Command to run tests without fetching or building anything.""" 80 81 description = "run tests without fetching or building anything." 82 user_options = [] 83 84 def initialize_options(self): 85 pass 86 87 def finalize_options(self): 88 # distutils requires this override. 89 pass 90 91 def run(self): 92 import tests 93 94 loader = tests.Loader() 95 loader.loadTestsFromNames(["tests"]) 96 runner = tests.Runner(dedicated_threads=True) 97 result = runner.run(loader.suite) 98 if not result.wasSuccessful(): 99 sys.exit("Test failure") 100 101 102class TestPy3Only(setuptools.Command): 103 """Command to run tests for Python 3+ features. 104 105 This does not include asyncio tests, which are housed in a separate 106 directory. 107 """ 108 109 description = "run tests for py3+ features" 110 user_options = [] 111 112 def initialize_options(self): 113 pass 114 115 def finalize_options(self): 116 pass 117 118 def run(self): 119 import tests 120 121 loader = tests.Loader() 122 loader.loadTestsFromNames(["tests_py3_only"]) 123 runner = tests.Runner() 124 result = runner.run(loader.suite) 125 if not result.wasSuccessful(): 126 sys.exit("Test failure") 127 128 129class TestAio(setuptools.Command): 130 """Command to run aio tests without fetching or building anything.""" 131 132 description = "run aio tests without fetching or building anything." 133 user_options = [] 134 135 def initialize_options(self): 136 pass 137 138 def finalize_options(self): 139 pass 140 141 def run(self): 142 import tests 143 144 loader = tests.Loader() 145 loader.loadTestsFromNames(["tests_aio"]) 146 # Even without dedicated threads, the framework will somehow spawn a 147 # new thread for tests to run upon. New thread doesn't have event loop 148 # attached by default, so initialization is needed. 149 runner = tests.Runner(dedicated_threads=False) 150 result = runner.run(loader.suite) 151 if not result.wasSuccessful(): 152 sys.exit("Test failure") 153 154 155class RunInterop(test.test): 156 description = "run interop test client/server" 157 user_options = [ 158 ("args=", None, "pass-thru arguments for the client/server"), 159 ("client", None, "flag indicating to run the client"), 160 ("server", None, "flag indicating to run the server"), 161 ("use-asyncio", None, "flag indicating to run the asyncio stack"), 162 ] 163 164 def initialize_options(self): 165 self.args = "" 166 self.client = False 167 self.server = False 168 self.use_asyncio = False 169 170 def finalize_options(self): 171 if self.client and self.server: 172 raise _errors.OptionError( 173 "you may only specify one of client or server" 174 ) 175 176 def run(self): 177 if self.client: 178 self.run_client() 179 elif self.server: 180 self.run_server() 181 182 def run_server(self): 183 # We import here to ensure that our setuptools parent has had a chance to 184 # edit the Python system path. 185 if self.use_asyncio: 186 import asyncio 187 188 from tests_aio.interop import server 189 190 sys.argv[1:] = self.args.split() 191 args = server.parse_interop_server_arguments(sys.argv) 192 asyncio.get_event_loop().run_until_complete(server.serve(args)) 193 else: 194 from tests.interop import server 195 196 sys.argv[1:] = self.args.split() 197 server.serve(server.parse_interop_server_arguments(sys.argv)) 198 199 def run_client(self): 200 # We import here to ensure that our setuptools parent has had a chance to 201 # edit the Python system path. 202 from tests.interop import client 203 204 sys.argv[1:] = self.args.split() 205 client.test_interoperability(client.parse_interop_client_args(sys.argv)) 206 207 208class RunFork(test.test): 209 description = "run fork test client" 210 user_options = [("args=", "a", "pass-thru arguments for the client")] 211 212 def initialize_options(self): 213 self.args = "" 214 215 def finalize_options(self): 216 # distutils requires this override. 217 pass 218 219 def run(self): 220 # We import here to ensure that our setuptools parent has had a chance to 221 # edit the Python system path. 222 from tests.fork import client 223 224 sys.argv[1:] = self.args.split() 225 client.test_fork() 226