Skip to content

Commit 980c64e

Browse files
crwilcoxkurtisvg
authored andcommitted
Add blacken session and create noxfile template (#2581)
* feature: create noxfile template for sub projects * fix: blacken both noxfiles * feature: add blacken target, remove ci diff helper * fix: match lint to black * fix: root detection bug * fix: remove requirements check from project specific noxfile * fix: remove missing test utility function * fix: remove unreferenced helper function * fix: simplify template * fix: run on multiple python versions for 3 * fix: rename py27 to py2, py36 to py3 * fix: adjust how we detect paths now that we do things in a subdirectory * fix: adjust header
1 parent e537797 commit 980c64e

File tree

2 files changed

+347
-89
lines changed

2 files changed

+347
-89
lines changed

noxfile-template.py

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Copyright 2019 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+
15+
from __future__ import print_function
16+
17+
import fnmatch
18+
import os
19+
from pathlib import Path
20+
import tempfile
21+
22+
import nox
23+
24+
# Get root of this repository. Assume we don't have directories nested deeper than 10 items.
25+
p = Path(os.getcwd())
26+
for i in range(10):
27+
if p is None:
28+
raise Exception("Unable to detect repository root.")
29+
if Path(p / ".git").exists():
30+
REPO_ROOT = str(p)
31+
break
32+
p = p.parent
33+
34+
#
35+
# Helpers and utility functions
36+
#
37+
38+
39+
def _list_files(folder, pattern):
40+
"""Lists all files below the given folder that match the pattern."""
41+
for root, folders, files in os.walk(folder):
42+
for filename in files:
43+
if fnmatch.fnmatch(filename, pattern):
44+
yield os.path.join(root, filename)
45+
46+
47+
def _collect_dirs(
48+
start_dir,
49+
blacklist=set(["conftest.py", "noxfile.py", "lib", "third_party"]),
50+
suffix="_test.py",
51+
recurse_further=False,
52+
):
53+
"""Recursively collects a list of dirs that contain a file matching the
54+
given suffix.
55+
56+
This works by listing the contents of directories and finding
57+
directories that have `*_test.py` files.
58+
"""
59+
# Collect all the directories that have tests in them.
60+
for parent, subdirs, files in os.walk(start_dir):
61+
if "./." in parent:
62+
continue # Skip top-level dotfiles
63+
elif any(f for f in files if f.endswith(suffix) and f not in blacklist):
64+
# Don't recurse further for tests, since py.test will do that.
65+
if not recurse_further:
66+
del subdirs[:]
67+
# This dir has desired files in it. yield it.
68+
yield parent
69+
else:
70+
# Filter out dirs we don't want to recurse into
71+
subdirs[:] = [s for s in subdirs if s[0].isalpha() and s not in blacklist]
72+
73+
74+
75+
def _determine_local_import_names(start_dir):
76+
"""Determines all import names that should be considered "local".
77+
78+
This is used when running the linter to insure that import order is
79+
properly checked.
80+
"""
81+
file_ext_pairs = [os.path.splitext(path) for path in os.listdir(start_dir)]
82+
return [
83+
basename
84+
for basename, extension in file_ext_pairs
85+
if extension == ".py"
86+
or os.path.isdir(os.path.join(start_dir, basename))
87+
and basename not in ("__pycache__")
88+
]
89+
90+
91+
#
92+
# App Engine specific helpers
93+
#
94+
95+
96+
_GAE_ROOT = os.environ.get("GAE_ROOT")
97+
if _GAE_ROOT is None:
98+
_GAE_ROOT = tempfile.mkdtemp()
99+
100+
101+
def _setup_appengine_sdk(session):
102+
"""Installs the App Engine SDK, if needed."""
103+
session.env["GAE_SDK_PATH"] = os.path.join(_GAE_ROOT, "google_appengine")
104+
session.run("gcp-devrel-py-tools", "download-appengine-sdk", _GAE_ROOT)
105+
106+
107+
#
108+
# Test sessions
109+
#
110+
111+
112+
PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"]
113+
114+
# Ignore I202 "Additional newline in a section of imports." to accommodate
115+
# region tags in import blocks. Since we specify an explicit ignore, we also
116+
# have to explicitly ignore the list of default ignores:
117+
# `E121,E123,E126,E226,E24,E704,W503,W504` as shown by `flake8 --help`.
118+
FLAKE8_COMMON_ARGS = [
119+
"--show-source",
120+
"--builtin",
121+
"gettext",
122+
"--max-complexity",
123+
"20",
124+
"--import-order-style",
125+
"google",
126+
"--exclude",
127+
".nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py",
128+
"--ignore=E121,E123,E126,E203, E226,E24,E266,E501,E704,W503,W504,I100,I201,I202",
129+
]
130+
131+
132+
# Collect sample directories.
133+
ALL_TESTED_SAMPLES = sorted(list(_collect_dirs(".")))
134+
135+
GAE_STANDARD_SAMPLES = [
136+
sample
137+
for sample in ALL_TESTED_SAMPLES
138+
if str(Path(sample).absolute().relative_to(REPO_ROOT)).startswith("appengine/standard/")
139+
]
140+
PY3_ONLY_SAMPLES = [
141+
sample
142+
for sample in ALL_TESTED_SAMPLES
143+
if (
144+
str(Path(sample).absolute().relative_to(REPO_ROOT)).startswith("appengine/standard_python37")
145+
or str(Path(sample).absolute().relative_to(REPO_ROOT)).startswith("functions/")
146+
or str(Path(sample).absolute().relative_to(REPO_ROOT)).startswith("bigquery/pandas-gbq-migration")
147+
)
148+
]
149+
NON_GAE_STANDARD_SAMPLES_PY2 = sorted(
150+
list((set(ALL_TESTED_SAMPLES) - set(GAE_STANDARD_SAMPLES)) - set(PY3_ONLY_SAMPLES))
151+
)
152+
NON_GAE_STANDARD_SAMPLES_PY3 = sorted(
153+
list(set(ALL_TESTED_SAMPLES) - set(GAE_STANDARD_SAMPLES))
154+
)
155+
156+
157+
def _session_tests(session, sample, post_install=None):
158+
"""Runs py.test for a particular sample."""
159+
session.install("-r", REPO_ROOT + "/testing/requirements.txt")
160+
161+
session.chdir(sample)
162+
163+
if os.path.exists("requirements.txt"):
164+
session.install("-r", "requirements.txt")
165+
166+
if post_install:
167+
post_install(session)
168+
169+
session.run(
170+
"pytest",
171+
*(PYTEST_COMMON_ARGS + session.posargs),
172+
# Pytest will return 5 when no tests are collected. This can happen
173+
# on travis where slow and flaky tests are excluded.
174+
# See http://doc.pytest.org/en/latest/_modules/_pytest/main.html
175+
success_codes=[0, 5]
176+
)
177+
178+
179+
@nox.session(python="2.7")
180+
@nox.parametrize("sample", GAE_STANDARD_SAMPLES)
181+
def gae(session, sample):
182+
"""Runs py.test for an App Engine standard sample."""
183+
184+
# Create a lib directory if needed, otherwise the App Engine vendor library
185+
# will complain.
186+
if not os.path.isdir(os.path.join(sample, "lib")):
187+
os.mkdir(os.path.join(sample, "lib"))
188+
189+
_session_tests(session, sample, _setup_appengine_sdk)
190+
191+
192+
@nox.session(python="2.7")
193+
@nox.parametrize("sample", NON_GAE_STANDARD_SAMPLES_PY2)
194+
def py2(session, sample):
195+
"""Runs py.test for a sample using Python 2.7"""
196+
_session_tests(session, sample)
197+
198+
199+
@nox.session(python=["3.5", "3.6", "3.7"])
200+
@nox.parametrize("sample", NON_GAE_STANDARD_SAMPLES_PY3)
201+
def py3(session, sample):
202+
"""Runs py.test for a sample using Python 3.x"""
203+
_session_tests(session, sample)
204+
205+
206+
@nox.session(python="3.6")
207+
def lint(session):
208+
"""Runs flake8 on the sample."""
209+
session.install("flake8", "flake8-import-order", BLACK_VERSION)
210+
211+
session.run("black", "--check", ".")
212+
213+
214+
local_names = _determine_local_import_names(".")
215+
args = FLAKE8_COMMON_ARGS + [
216+
"--application-import-names",
217+
",".join(local_names),
218+
".",
219+
]
220+
session.run("flake8", *args)
221+
222+
223+
224+
BLACK_VERSION = "black==19.3b0"
225+
226+
227+
@nox.session(python="3.6")
228+
def blacken(session):
229+
"""Run black.
230+
Format code to uniform standard.
231+
"""
232+
session.install(BLACK_VERSION)
233+
session.run("black", ".")
234+
235+
236+
237+
SAMPLES_WITH_GENERATED_READMES = sorted(list(_collect_dirs(".", suffix=".rst.in")))
238+
239+
240+
@nox.session
241+
@nox.parametrize("sample", SAMPLES_WITH_GENERATED_READMES)
242+
def readmegen(session, sample):
243+
"""(Re-)generates the readme for a sample."""
244+
session.install("jinja2", "pyyaml")
245+
246+
if os.path.exists(os.path.join(sample, "requirements.txt")):
247+
session.install("-r", os.path.join(sample, "requirements.txt"))
248+
249+
in_file = os.path.join(sample, "README.rst.in")
250+
session.run("python", REPO_ROOT + "/scripts/readme-gen/readme_gen.py", in_file)

0 commit comments

Comments
 (0)