1# Copyright 2016 Google LLC 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 15from __future__ import absolute_import 16import os 17import pathlib 18import re 19import shutil 20import unittest 21 22# https://github.com/google/importlab/issues/25 23import nox # pytype: disable=import-error 24 25 26BLACK_VERSION = "black==22.3.0" 27BLACK_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] 28# Black and flake8 clash on the syntax for ignoring flake8's F401 in this file. 29BLACK_EXCLUDES = ["--exclude", "^/google/api_core/operations_v1/__init__.py"] 30 31PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 32 33DEFAULT_PYTHON_VERSION = "3.10" 34CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() 35 36# 'docfx' is excluded since it only needs to run in 'docs-presubmit' 37nox.options.sessions = [ 38 "unit", 39 "unit_grpc_gcp", 40 "unit_wo_grpc", 41 "unit_w_prerelease_deps", 42 "unit_w_async_rest_extra", 43 "cover", 44 "pytype", 45 "mypy", 46 "lint", 47 "lint_setup_py", 48 "blacken", 49 "docs", 50] 51 52# Error if a python version is missing 53nox.options.error_on_missing_interpreters = True 54 55 56@nox.session(python=DEFAULT_PYTHON_VERSION) 57def lint(session): 58 """Run linters. 59 60 Returns a failure if the linters find linting errors or sufficiently 61 serious code quality issues. 62 """ 63 session.install("flake8", BLACK_VERSION) 64 session.install(".") 65 session.run( 66 "black", 67 "--check", 68 *BLACK_EXCLUDES, 69 *BLACK_PATHS, 70 ) 71 session.run("flake8", "google", "tests") 72 73 74@nox.session(python=DEFAULT_PYTHON_VERSION) 75def blacken(session): 76 """Run black. 77 78 Format code to uniform standard. 79 """ 80 session.install(BLACK_VERSION) 81 session.run("black", *BLACK_EXCLUDES, *BLACK_PATHS) 82 83 84def install_prerelease_dependencies(session, constraints_path): 85 with open(constraints_path, encoding="utf-8") as constraints_file: 86 constraints_text = constraints_file.read() 87 # Ignore leading whitespace and comment lines. 88 constraints_deps = [ 89 match.group(1) 90 for match in re.finditer( 91 r"^\s*(\S+)(?===\S+)", constraints_text, flags=re.MULTILINE 92 ) 93 ] 94 session.install(*constraints_deps) 95 prerel_deps = [ 96 "google-auth", 97 "googleapis-common-protos", 98 # Exclude grpcio!=1.67.0rc1 which does not support python 3.13 99 "grpcio!=1.67.0rc1", 100 "grpcio-status", 101 "proto-plus", 102 "protobuf", 103 ] 104 105 for dep in prerel_deps: 106 session.install("--pre", "--no-deps", "--upgrade", dep) 107 108 # Remaining dependencies 109 other_deps = [ 110 "requests", 111 ] 112 session.install(*other_deps) 113 114 115def default(session, install_grpc=True, prerelease=False, install_async_rest=False): 116 """Default unit test session. 117 118 This is intended to be run **without** an interpreter set, so 119 that the current ``python`` (on the ``PATH``) or the version of 120 Python corresponding to the ``nox`` binary the ``PATH`` can 121 run the tests. 122 """ 123 if prerelease and not install_grpc: 124 unittest.skip("The pre-release session cannot be run without grpc") 125 126 session.install( 127 "dataclasses", 128 "mock; python_version=='3.7'", 129 "pytest", 130 "pytest-cov", 131 "pytest-xdist", 132 ) 133 134 install_extras = [] 135 if install_grpc: 136 # Note: The extra is called `grpc` and not `grpcio`. 137 install_extras.append("grpc") 138 139 constraints_dir = str(CURRENT_DIRECTORY / "testing") 140 if install_async_rest: 141 install_extras.append("async_rest") 142 constraints_type = "async-rest-" 143 else: 144 constraints_type = "" 145 146 lib_with_extras = f".[{','.join(install_extras)}]" if len(install_extras) else "." 147 if prerelease: 148 install_prerelease_dependencies( 149 session, 150 f"{constraints_dir}/constraints-{constraints_type}{PYTHON_VERSIONS[0]}.txt", 151 ) 152 # This *must* be the last install command to get the package from source. 153 session.install("-e", lib_with_extras, "--no-deps") 154 else: 155 constraints_file = ( 156 f"{constraints_dir}/constraints-{constraints_type}{session.python}.txt" 157 ) 158 # fall back to standard constraints file 159 if not pathlib.Path(constraints_file).exists(): 160 constraints_file = f"{constraints_dir}/constraints-{session.python}.txt" 161 162 session.install( 163 "-e", 164 lib_with_extras, 165 "-c", 166 constraints_file, 167 ) 168 169 # Print out package versions of dependencies 170 session.run( 171 "python", "-c", "import google.protobuf; print(google.protobuf.__version__)" 172 ) 173 # Support for proto.version was added in v1.23.0 174 # https://github.com/googleapis/proto-plus-python/releases/tag/v1.23.0 175 session.run( 176 "python", 177 "-c", 178 """import proto; hasattr(proto, "version") and print(proto.version.__version__)""", 179 ) 180 if install_grpc: 181 session.run("python", "-c", "import grpc; print(grpc.__version__)") 182 session.run("python", "-c", "import google.auth; print(google.auth.__version__)") 183 184 pytest_args = [ 185 "python", 186 "-m", 187 "pytest", 188 *( 189 # Helpful for running a single test or testfile. 190 session.posargs 191 or [ 192 "--quiet", 193 "--cov=google.api_core", 194 "--cov=tests.unit", 195 "--cov-append", 196 "--cov-config=.coveragerc", 197 "--cov-report=", 198 "--cov-fail-under=0", 199 # Running individual tests with parallelism enabled is usually not helpful. 200 "-n=auto", 201 os.path.join("tests", "unit"), 202 ] 203 ), 204 ] 205 206 session.install("asyncmock", "pytest-asyncio") 207 208 # Having positional arguments means the user wants to run specific tests. 209 # Best not to add additional tests to that list. 210 if not session.posargs: 211 pytest_args.append("--cov=tests.asyncio") 212 pytest_args.append(os.path.join("tests", "asyncio")) 213 214 session.run(*pytest_args) 215 216 217@nox.session(python=PYTHON_VERSIONS) 218def unit(session): 219 """Run the unit test suite.""" 220 default(session) 221 222 223@nox.session(python=PYTHON_VERSIONS) 224def unit_w_prerelease_deps(session): 225 """Run the unit test suite.""" 226 default(session, prerelease=True) 227 228 229@nox.session(python=PYTHON_VERSIONS) 230def unit_grpc_gcp(session): 231 """ 232 Run the unit test suite with grpcio-gcp installed. 233 `grpcio-gcp` doesn't support protobuf 4+. 234 Remove extra `grpcgcp` when protobuf 3.x is dropped. 235 https://github.com/googleapis/python-api-core/issues/594 236 """ 237 constraints_path = str( 238 CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" 239 ) 240 # Install grpcio-gcp 241 session.install("-e", ".[grpcgcp]", "-c", constraints_path) 242 # Install protobuf < 4.0.0 243 session.install("protobuf<4.0.0") 244 245 default(session) 246 247 248@nox.session(python=PYTHON_VERSIONS) 249def unit_wo_grpc(session): 250 """Run the unit test suite w/o grpcio installed""" 251 default(session, install_grpc=False) 252 253 254@nox.session(python=PYTHON_VERSIONS) 255def unit_w_async_rest_extra(session): 256 """Run the unit test suite with the `async_rest` extra""" 257 default(session, install_async_rest=True) 258 259 260@nox.session(python=DEFAULT_PYTHON_VERSION) 261def lint_setup_py(session): 262 """Verify that setup.py is valid (including RST check).""" 263 264 session.install("docutils", "Pygments") 265 session.run("python", "setup.py", "check", "--restructuredtext", "--strict") 266 267 268@nox.session(python=DEFAULT_PYTHON_VERSION) 269def pytype(session): 270 """Run type-checking.""" 271 session.install(".[grpc]", "pytype") 272 session.run("pytype") 273 274 275@nox.session(python=DEFAULT_PYTHON_VERSION) 276def mypy(session): 277 """Run type-checking.""" 278 session.install(".[grpc,async_rest]", "mypy") 279 session.install( 280 "types-setuptools", 281 "types-requests", 282 "types-protobuf", 283 "types-dataclasses", 284 "types-mock; python_version=='3.7'", 285 ) 286 session.run("mypy", "google", "tests") 287 288 289@nox.session(python=DEFAULT_PYTHON_VERSION) 290def cover(session): 291 """Run the final coverage report. 292 293 This outputs the coverage report aggregating coverage from the unit 294 test runs (not system test runs), and then erases coverage data. 295 """ 296 session.install("coverage", "pytest-cov") 297 session.run("coverage", "report", "--show-missing", "--fail-under=100") 298 session.run("coverage", "erase") 299 300 301@nox.session(python="3.10") 302def docs(session): 303 """Build the docs for this library.""" 304 305 session.install("-e", ".[grpc]") 306 session.install( 307 # We need to pin to specific versions of the `sphinxcontrib-*` packages 308 # which still support sphinx 4.x. 309 # See https://github.com/googleapis/sphinx-docfx-yaml/issues/344 310 # and https://github.com/googleapis/sphinx-docfx-yaml/issues/345. 311 "sphinxcontrib-applehelp==1.0.4", 312 "sphinxcontrib-devhelp==1.0.2", 313 "sphinxcontrib-htmlhelp==2.0.1", 314 "sphinxcontrib-qthelp==1.0.3", 315 "sphinxcontrib-serializinghtml==1.1.5", 316 "sphinx==4.5.0", 317 "alabaster", 318 "recommonmark", 319 ) 320 321 shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) 322 session.run( 323 "sphinx-build", 324 "-W", # warnings as errors 325 "-T", # show full traceback on exception 326 "-N", # no colors 327 "-b", 328 "html", 329 "-d", 330 os.path.join("docs", "_build", "doctrees", ""), 331 os.path.join("docs", ""), 332 os.path.join("docs", "_build", "html", ""), 333 ) 334 335 336@nox.session(python="3.10") 337def docfx(session): 338 """Build the docfx yaml files for this library.""" 339 340 session.install("-e", ".") 341 session.install( 342 # We need to pin to specific versions of the `sphinxcontrib-*` packages 343 # which still support sphinx 4.x. 344 # See https://github.com/googleapis/sphinx-docfx-yaml/issues/344 345 # and https://github.com/googleapis/sphinx-docfx-yaml/issues/345. 346 "sphinxcontrib-applehelp==1.0.4", 347 "sphinxcontrib-devhelp==1.0.2", 348 "sphinxcontrib-htmlhelp==2.0.1", 349 "sphinxcontrib-qthelp==1.0.3", 350 "sphinxcontrib-serializinghtml==1.1.5", 351 "gcp-sphinx-docfx-yaml", 352 "alabaster", 353 "recommonmark", 354 ) 355 356 shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) 357 session.run( 358 "sphinx-build", 359 "-T", # show full traceback on exception 360 "-N", # no colors 361 "-D", 362 ( 363 "extensions=sphinx.ext.autodoc," 364 "sphinx.ext.autosummary," 365 "docfx_yaml.extension," 366 "sphinx.ext.intersphinx," 367 "sphinx.ext.coverage," 368 "sphinx.ext.napoleon," 369 "sphinx.ext.todo," 370 "sphinx.ext.viewcode," 371 "recommonmark" 372 ), 373 "-b", 374 "html", 375 "-d", 376 os.path.join("docs", "_build", "doctrees", ""), 377 os.path.join("docs", ""), 378 os.path.join("docs", "_build", "html", ""), 379 ) 380