Skip to content

Commit 5c04647

Browse files
authored
Merge pull request googleapis#2498 from dhermes/compute-dep-graph-2
Follow package dependency graph from changed packages.
2 parents e7fd064 + 8ee173c commit 5c04647

File tree

6 files changed

+145
-18
lines changed

6 files changed

+145
-18
lines changed

scripts/generate_json_docs.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from parinx.errors import MethodParsingException
2727
import six
2828

29+
from script_utils import PROJECT_ROOT
2930
from verify_included_modules import get_public_modules
3031

3132

@@ -601,7 +602,7 @@ def main():
601602
parser.add_argument('--tag', help='The version of the documentation.',
602603
default='master')
603604
parser.add_argument('--basepath', help='Path to the library.',
604-
default=os.path.join(os.path.dirname(__file__), '..'))
605+
default=PROJECT_ROOT)
605606
parser.add_argument('--show-toc', help='Prints partial table of contents',
606607
default=False)
607608
args = parser.parse_args()
@@ -635,18 +636,17 @@ def main():
635636
}
636637
}
637638

638-
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
639-
BASE_JSON_DOCS_DIR = os.path.join(BASE_DIR, 'docs', 'json')
639+
BASE_JSON_DOCS_DIR = os.path.join(PROJECT_ROOT, 'docs', 'json')
640640

641-
DOCS_BUILD_DIR = os.path.join(BASE_DIR, 'docs', '_build')
641+
DOCS_BUILD_DIR = os.path.join(PROJECT_ROOT, 'docs', '_build')
642642
JSON_DOCS_DIR = os.path.join(DOCS_BUILD_DIR, 'json', args.tag)
643643
LIB_DIR = os.path.abspath(args.basepath)
644644

645645
library_dir = os.path.join(LIB_DIR, 'google', 'cloud')
646646
public_mods = get_public_modules(library_dir,
647647
base_package='google.cloud')
648648

649-
generate_module_docs(public_mods, JSON_DOCS_DIR, BASE_DIR, toc)
649+
generate_module_docs(public_mods, JSON_DOCS_DIR, PROJECT_ROOT, toc)
650650
generate_doc_types_json(public_mods,
651651
os.path.join(JSON_DOCS_DIR, 'types.json'))
652652
package_files(JSON_DOCS_DIR, DOCS_BUILD_DIR, BASE_JSON_DOCS_DIR)

scripts/make_datastore_grpc.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
import sys
2121
import tempfile
2222

23+
from script_utils import PROJECT_ROOT
2324

24-
ROOT_DIR = os.path.abspath(
25-
os.path.join(os.path.dirname(__file__), '..'))
26-
PROTOS_DIR = os.path.join(ROOT_DIR, 'googleapis-pb')
25+
26+
PROTOS_DIR = os.path.join(PROJECT_ROOT, 'googleapis-pb')
2727
PROTO_PATH = os.path.join(PROTOS_DIR, 'google', 'datastore',
2828
'v1', 'datastore.proto')
29-
GRPC_ONLY_FILE = os.path.join(ROOT_DIR, 'datastore',
29+
GRPC_ONLY_FILE = os.path.join(PROJECT_ROOT, 'datastore',
3030
'google', 'cloud', 'datastore',
3131
'_generated', 'datastore_grpc_pb2.py')
3232
GRPCIO_VIRTUALENV = os.getenv('GRPCIO_VIRTUALENV')

scripts/run_pylint.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import sys
3131

3232
from script_utils import get_affected_files
33+
from script_utils import PROJECT_ROOT
3334

3435

3536
IGNORED_DIRECTORIES = [
@@ -44,7 +45,7 @@
4445
os.path.join('google', 'cloud', '__init__.py'),
4546
'setup.py',
4647
]
47-
SCRIPTS_DIR = os.path.abspath(os.path.dirname(__file__))
48+
SCRIPTS_DIR = os.path.join(PROJECT_ROOT, 'scripts')
4849
PRODUCTION_RC = os.path.join(SCRIPTS_DIR, 'pylintrc_default')
4950
TEST_RC = os.path.join(SCRIPTS_DIR, 'pylintrc_reduced')
5051
TEST_DISABLED_MESSAGES = [

scripts/run_unit_tests.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727
import sys
2828

2929
from script_utils import check_output
30+
from script_utils import follow_dependencies
3031
from script_utils import get_changed_packages
3132
from script_utils import in_travis
3233
from script_utils import in_travis_pr
3334
from script_utils import local_diff_branch
35+
from script_utils import PROJECT_ROOT
3436
from script_utils import travis_branch
3537

3638

37-
PROJECT_ROOT = os.path.abspath(
38-
os.path.join(os.path.dirname(__file__), '..'))
3939
IGNORED_DIRECTORIES = (
4040
'appveyor',
4141
'docs',
@@ -127,6 +127,12 @@ def get_test_packages():
127127
any filtering)
128128
* Just use all packages
129129
130+
An additional check is done for the cases when a diff is computed (i.e.
131+
using local remote and local branch environment variables, and on Travis).
132+
Once the filtered list of **changed** packages is found, the package
133+
dependency graph is used to add any additional packages which depend on
134+
the changed packages.
135+
130136
:rtype: list
131137
:returns: A list of all package directories where tests
132138
need be run.
@@ -140,9 +146,12 @@ def get_test_packages():
140146
verify_packages(args.packages, all_packages)
141147
return sorted(args.packages)
142148
elif local_diff is not None:
143-
return get_changed_packages('HEAD', local_diff, all_packages)
149+
changed_packages = get_changed_packages(
150+
'HEAD', local_diff, all_packages)
151+
return follow_dependencies(changed_packages, all_packages)
144152
elif in_travis():
145-
return get_travis_directories(all_packages)
153+
changed_packages = get_travis_directories(all_packages)
154+
return follow_dependencies(changed_packages, all_packages)
146155
else:
147156
return all_packages
148157

scripts/script_utils.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@
1616

1717
from __future__ import print_function
1818

19+
import ast
1920
import os
2021
import subprocess
2122

2223

24+
PROJECT_ROOT = os.path.abspath(
25+
os.path.join(os.path.dirname(__file__), '..'))
2326
LOCAL_REMOTE_ENV = 'GOOGLE_CLOUD_TESTING_REMOTE'
2427
LOCAL_BRANCH_ENV = 'GOOGLE_CLOUD_TESTING_BRANCH'
2528
IN_TRAVIS_ENV = 'TRAVIS'
2629
TRAVIS_PR_ENV = 'TRAVIS_PULL_REQUEST'
2730
TRAVIS_BRANCH_ENV = 'TRAVIS_BRANCH'
31+
INST_REQS_KWARG = 'install_requires'
32+
REQ_VAR = 'REQUIREMENTS'
33+
PACKAGE_PREFIX = 'google-cloud-'
2834

2935

3036
def in_travis():
@@ -226,3 +232,114 @@ def get_affected_files(allow_limited=True):
226232
result = subprocess.check_output(['git', 'ls-files'])
227233

228234
return result.rstrip('\n').split('\n'), diff_base
235+
236+
237+
def get_required_packages(file_contents):
238+
"""Get required packages from a ``setup.py`` file.
239+
240+
Makes the following assumptions:
241+
242+
* ``install_requires=REQUIREMENTS`` occurs in the call to
243+
``setup()`` in the ``file_contents``.
244+
* The text ``install_requires`` occurs nowhere else in the file.
245+
* The text ``REQUIREMENTS`` only appears when being passed to
246+
``setup()`` (as above) and when being defined.
247+
* The ``REQUIREMENTS`` variable is a list and the text from the
248+
``setup.py`` file containing that list can be parsed using
249+
``ast.literal_eval()``.
250+
251+
:type file_contents: str
252+
:param file_contents: The contents of a ``setup.py`` file.
253+
254+
:rtype: list
255+
:returns: The list of required packages.
256+
:raises: :class:`~exceptions.ValueError` if the file is in an
257+
unexpected format.
258+
"""
259+
# Make sure the only ``install_requires`` happens in the
260+
# call to setup()
261+
if file_contents.count(INST_REQS_KWARG) != 1:
262+
raise ValueError('Expected only one use of keyword',
263+
INST_REQS_KWARG, file_contents)
264+
# Make sure the only usage of ``install_requires`` is to set
265+
# install_requires=REQUIREMENTS.
266+
keyword_stmt = INST_REQS_KWARG + '=' + REQ_VAR
267+
if file_contents.count(keyword_stmt) != 1:
268+
raise ValueError('Expected keyword to be set with variable',
269+
INST_REQS_KWARG, REQ_VAR, file_contents)
270+
# Split file on ``REQUIREMENTS`` variable while asserting that
271+
# it only appear twice.
272+
_, reqs_section, _ = file_contents.split(REQ_VAR)
273+
# Find ``REQUIREMENTS`` list variable defined in ``reqs_section``.
274+
reqs_begin = reqs_section.index('[')
275+
reqs_end = reqs_section.index(']') + 1
276+
277+
# Convert the text to an actual list, but make sure no
278+
# locals or globals can be used.
279+
reqs_list_text = reqs_section[reqs_begin:reqs_end]
280+
# We use literal_eval() because it limits to evaluating
281+
# strings that only consist of a few Python literals: strings,
282+
# numbers, tuples, lists, dicts, booleans, and None.
283+
requirements = ast.literal_eval(reqs_list_text)
284+
285+
# Take the list of requirements and strip off the package name
286+
# from each requirement.
287+
result = []
288+
for required in requirements:
289+
parts = required.split()
290+
result.append(parts[0])
291+
return result
292+
293+
294+
def get_dependency_graph(package_list):
295+
"""Get a directed graph of package dependencies.
296+
297+
:type package_list: list
298+
:param package_list: The list of **all** valid packages.
299+
300+
:rtype: dict
301+
:returns: A dictionary where keys are packages and values are
302+
the set of packages that depend on the key.
303+
"""
304+
result = {package: set() for package in package_list}
305+
for package in package_list:
306+
setup_file = os.path.join(PROJECT_ROOT, package,
307+
'setup.py')
308+
with open(setup_file, 'r') as file_obj:
309+
file_contents = file_obj.read()
310+
311+
requirements = get_required_packages(file_contents)
312+
for requirement in requirements:
313+
if not requirement.startswith(PACKAGE_PREFIX):
314+
continue
315+
_, req_package = requirement.split(PACKAGE_PREFIX)
316+
req_package = req_package.replace('-', '_')
317+
result[req_package].add(package)
318+
319+
return result
320+
321+
322+
def follow_dependencies(subset, package_list):
323+
"""Get a directed graph of package dependencies.
324+
325+
:type subset: list
326+
:param subset: List of a subset of package names.
327+
328+
:type package_list: list
329+
:param package_list: The list of **all** valid packages.
330+
331+
:rtype: list
332+
:returns: An expanded list of packages containing everything
333+
in ``subset`` and any packages that depend on those.
334+
"""
335+
dependency_graph = get_dependency_graph(package_list)
336+
337+
curr_pkgs = None
338+
updated_pkgs = set(subset)
339+
while curr_pkgs != updated_pkgs:
340+
curr_pkgs = updated_pkgs
341+
updated_pkgs = set(curr_pkgs)
342+
for package in curr_pkgs:
343+
updated_pkgs.update(dependency_graph[package])
344+
345+
return sorted(curr_pkgs)

scripts/verify_included_modules.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424

2525
from sphinx.ext.intersphinx import fetch_inventory
2626

27+
from script_utils import PROJECT_ROOT
2728

28-
BASE_DIR = os.path.abspath(
29-
os.path.join(os.path.dirname(__file__), '..'))
30-
DOCS_DIR = os.path.join(BASE_DIR, 'docs')
29+
30+
DOCS_DIR = os.path.join(PROJECT_ROOT, 'docs')
3131
IGNORED_PREFIXES = ('test_', '_')
3232
IGNORED_MODULES = frozenset([
3333
'google.cloud.__init__',
@@ -153,7 +153,7 @@ def verify_modules(build_root='_build'):
153153

154154
public_mods = set()
155155
for package in PACKAGES:
156-
library_dir = os.path.join(BASE_DIR, package, 'google', 'cloud')
156+
library_dir = os.path.join(PROJECT_ROOT, package, 'google', 'cloud')
157157
package_mods = get_public_modules(library_dir,
158158
base_package='google.cloud')
159159
public_mods.update(package_mods)

0 commit comments

Comments
 (0)