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 shutil
19
20# https://github.com/google/importlab/issues/25
21import nox  # pytype: disable=import-error
22
23
24BLACK_VERSION = "black==19.10b0"
25BLACK_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"]
26# Black and flake8 clash on the syntax for ignoring flake8's F401 in this file.
27BLACK_EXCLUDES = ["--exclude", "^/google/api_core/operations_v1/__init__.py"]
28
29DEFAULT_PYTHON_VERSION = "3.7"
30CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
31
32# 'docfx' is excluded since it only needs to run in 'docs-presubmit'
33nox.options.sessions = [
34    "unit",
35    "unit_grpc_gcp",
36    "unit_wo_grpc",
37    "cover",
38    "pytype",
39    "mypy",
40    "lint",
41    "lint_setup_py",
42    "blacken",
43    "docs",
44]
45
46
47def _greater_or_equal_than_36(version_string):
48    tokens = version_string.split(".")
49    for i, token in enumerate(tokens):
50        try:
51            tokens[i] = int(token)
52        except ValueError:
53            pass
54    return tokens >= [3, 6]
55
56
57@nox.session(python=DEFAULT_PYTHON_VERSION)
58def lint(session):
59    """Run linters.
60
61    Returns a failure if the linters find linting errors or sufficiently
62    serious code quality issues.
63    """
64    session.install("flake8", "flake8-import-order", BLACK_VERSION)
65    session.install(".")
66    session.run(
67        "black", "--check", *BLACK_EXCLUDES, *BLACK_PATHS,
68    )
69    session.run("flake8", "google", "tests")
70
71
72@nox.session(python=DEFAULT_PYTHON_VERSION)
73def blacken(session):
74    """Run black.
75
76    Format code to uniform standard.
77    """
78    session.install(BLACK_VERSION)
79    session.run("black", *BLACK_EXCLUDES, *BLACK_PATHS)
80
81
82def default(session, install_grpc=True):
83    """Default unit test session.
84
85    This is intended to be run **without** an interpreter set, so
86    that the current ``python`` (on the ``PATH``) or the version of
87    Python corresponding to the ``nox`` binary the ``PATH`` can
88    run the tests.
89    """
90    constraints_path = str(
91        CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
92    )
93
94    # Install all test dependencies, then install this package in-place.
95    session.install("mock", "pytest", "pytest-cov")
96    if install_grpc:
97        session.install("-e", ".[grpc]", "-c", constraints_path)
98    else:
99        session.install("-e", ".", "-c", constraints_path)
100
101    pytest_args = [
102        "python",
103        "-m",
104        "py.test",
105        "--quiet",
106        "--cov=google.api_core",
107        "--cov=tests.unit",
108        "--cov-append",
109        "--cov-config=.coveragerc",
110        "--cov-report=",
111        "--cov-fail-under=0",
112        os.path.join("tests", "unit"),
113    ]
114    pytest_args.extend(session.posargs)
115
116    # Inject AsyncIO content and proto-plus, if version >= 3.6.
117    # proto-plus is needed for a field mask test in test_protobuf_helpers.py
118    if _greater_or_equal_than_36(session.python):
119        session.install("asyncmock", "pytest-asyncio", "proto-plus")
120
121        pytest_args.append("--cov=tests.asyncio")
122        pytest_args.append(os.path.join("tests", "asyncio"))
123        session.run(*pytest_args)
124    else:
125        # Run py.test against the unit tests.
126        session.run(*pytest_args)
127
128
129@nox.session(python=["3.6", "3.7", "3.8", "3.9", "3.10"])
130def unit(session):
131    """Run the unit test suite."""
132    default(session)
133
134
135@nox.session(python=["3.6", "3.7", "3.8", "3.9"])
136def unit_grpc_gcp(session):
137    """Run the unit test suite with grpcio-gcp installed."""
138    constraints_path = str(
139        CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
140    )
141    # Install grpcio-gcp
142    session.install("-e", ".[grpcgcp]", "-c", constraints_path)
143
144    default(session)
145
146
147@nox.session(python=["3.6", "3.10"])
148def unit_wo_grpc(session):
149    """Run the unit test suite w/o grpcio installed"""
150    default(session, install_grpc=False)
151
152
153@nox.session(python="3.6")
154def lint_setup_py(session):
155    """Verify that setup.py is valid (including RST check)."""
156
157    session.install("docutils", "Pygments")
158    session.run("python", "setup.py", "check", "--restructuredtext", "--strict")
159
160
161# No 3.7 because pytype supports up to 3.6 only.
162@nox.session(python="3.6")
163def pytype(session):
164    """Run type-checking."""
165    session.install(".[grpc, grpcgcp]", "pytype >= 2019.3.21")
166    session.run("pytype")
167
168
169@nox.session(python=DEFAULT_PYTHON_VERSION)
170def mypy(session):
171    """Run type-checking."""
172    session.install(".[grpc, grpcgcp]", "mypy")
173    session.install(
174        "types-setuptools", "types-requests", "types-protobuf", "types-mock"
175    )
176    session.run("mypy", "google", "tests")
177
178
179@nox.session(python="3.6")
180def cover(session):
181    """Run the final coverage report.
182
183    This outputs the coverage report aggregating coverage from the unit
184    test runs (not system test runs), and then erases coverage data.
185    """
186    session.install("coverage", "pytest-cov")
187    session.run("coverage", "report", "--show-missing", "--fail-under=100")
188    session.run("coverage", "erase")
189
190
191@nox.session(python="3.8")
192def docs(session):
193    """Build the docs for this library."""
194
195    session.install("-e", ".[grpc, grpcgcp]")
196    session.install("sphinx==4.0.1", "alabaster", "recommonmark")
197
198    shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True)
199    session.run(
200        "sphinx-build",
201        "-W",  # warnings as errors
202        "-T",  # show full traceback on exception
203        "-N",  # no colors
204        "-b",
205        "html",
206        "-d",
207        os.path.join("docs", "_build", "doctrees", ""),
208        os.path.join("docs", ""),
209        os.path.join("docs", "_build", "html", ""),
210    )
211
212
213@nox.session(python="3.8")
214def docfx(session):
215    """Build the docfx yaml files for this library."""
216
217    session.install("-e", ".")
218    session.install(
219        "sphinx==4.0.1", "alabaster", "recommonmark", "gcp-sphinx-docfx-yaml"
220    )
221
222    shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True)
223    session.run(
224        "sphinx-build",
225        "-T",  # show full traceback on exception
226        "-N",  # no colors
227        "-D",
228        (
229            "extensions=sphinx.ext.autodoc,"
230            "sphinx.ext.autosummary,"
231            "docfx_yaml.extension,"
232            "sphinx.ext.intersphinx,"
233            "sphinx.ext.coverage,"
234            "sphinx.ext.napoleon,"
235            "sphinx.ext.todo,"
236            "sphinx.ext.viewcode,"
237            "recommonmark"
238        ),
239        "-b",
240        "html",
241        "-d",
242        os.path.join("docs", "_build", "doctrees", ""),
243        os.path.join("docs", ""),
244        os.path.join("docs", "_build", "html", ""),
245    )
246