• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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