From 86ed43d4f56e6404d068e62e497029018879c771 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Tue, 23 Mar 2021 10:52:02 -0600 Subject: [PATCH] test(python): use constraints files to check dependency lower bounds (#869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use a constraints file when installing dependencies for system and unit tests nox sessions. https://pip.pypa.io/en/stable/user_guide/#constraints-files > Constraints files are requirements files that **only control which version of a requirement is installed, not whether it is installed or not**. Their syntax and contents is nearly identical to Requirements Files. There is one key difference: Including a package in a constraints file does not trigger installation of the package. ``` testing ├── constraints-3.10.txt ├── constraints-3.11.txt ├── constraints-3.6.txt ├── constraints-3.7.txt ├── constraints-3.8.txt └── constraints-3.9.txt ``` Going forward, one constraints file (currently 3.6) will be populated with every library requirement and extra listed in the `setup.py`. The constraints file will pin each requirement to the lower bound. This ensures that library maintainers will see test failures if they forget to update a lower bound on a dependency. See https://github.com/googleapis/python-bigquery/pull/263 for an example --- .../templates/python_library/noxfile.py.j2 | 33 ++++++++++++------- tests/test_python_library.py | 22 ++++++------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/synthtool/gcp/templates/python_library/noxfile.py.j2 b/synthtool/gcp/templates/python_library/noxfile.py.j2 index 9bd5e7d89..149fd5a84 100644 --- a/synthtool/gcp/templates/python_library/noxfile.py.j2 +++ b/synthtool/gcp/templates/python_library/noxfile.py.j2 @@ -18,6 +18,7 @@ from __future__ import absolute_import import os +import pathlib import shutil import nox @@ -30,6 +31,8 @@ DEFAULT_PYTHON_VERSION="{{ default_python_version }}" SYSTEM_TEST_PYTHON_VERSIONS=[{% for v in system_test_python_versions %}"{{v}}"{% if not loop.last %},{% endif %}{% endfor %}] UNIT_TEST_PYTHON_VERSIONS=[{% for v in unit_test_python_versions %}"{{v}}"{% if not loop.last %},{% endif %}{% endfor %}] +CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() + # 'docfx' is excluded since it only needs to run in 'docs-presubmit' nox.options.sessions = [ "unit", @@ -88,24 +91,27 @@ def lint_setup_py(session): def default(session): # Install all test dependencies, then install this package in-place. + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) {%- if microgenerator %} - session.install("asyncmock", "pytest-asyncio") + session.install("asyncmock", "pytest-asyncio", "-c", constraints_path) {% endif %} - session.install("mock", "pytest", "pytest-cov", {% for d in unit_test_external_dependencies %}"{{d}}"{% if not loop.last %},{% endif %}{% endfor %}) - {% for dependency in unit_test_local_dependencies %}session.install("-e", "{{dependency}}") + session.install("mock", "pytest", "pytest-cov", {% for d in unit_test_external_dependencies %}"{{d}}",{% endfor %} "-c", constraints_path) + {% for dependency in unit_test_local_dependencies %}session.install("-e", "{{dependency}}", "-c", constraints_path) {% endfor %} - {% for dependency in unit_test_dependencies %}session.install("-e", "{{dependency}}"){% endfor %} + {% for dependency in unit_test_dependencies %}session.install("-e", "{{dependency}}", "-c", constraints_path){% endfor %} {%- if unit_test_extras_by_python %} {% for extras_python in unit_test_extras_by_python %} {%- if not loop.first %}el{% endif %}if session.python == "{{extras_python}}": extras = "[{{",".join(unit_test_extras_by_python[extras_python])}}]" {% endfor %}else: extras = "{%- if unit_test_extras %}[{{",".join(unit_test_extras)}}]{% endif %}" - session.install("-e", f".{extras}") + session.install("-e", f".{extras}", "-c", constraints_path) {% elif unit_test_extras %} - session.install("-e", ".[{{",".join(unit_test_extras)}}]") + session.install("-e", ".[{{",".join(unit_test_extras)}}]", "-c", constraints_path) {% else %} - session.install("-e", ".") + session.install("-e", ".", "-c", constraints_path) {% endif %} # Run py.test against the unit tests. @@ -132,6 +138,9 @@ def unit(session): @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def system(session): """Run the system test suite.""" + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) system_test_path = os.path.join("tests", "system.py") system_test_folder_path = os.path.join("tests", "system") @@ -156,10 +165,10 @@ def system(session): # Install all test dependencies, then install this package into the # virtualenv's dist-packages. - session.install("mock", "pytest", "google-cloud-testutils", {% for d in system_test_external_dependencies %}"{{d}}"{% if not loop.last %},{% endif %}{% endfor %}) + session.install("mock", "pytest", "google-cloud-testutils"{% for d in system_test_external_dependencies %}, "{{d}}"{% endfor %}, "-c", constraints_path) {%- if system_test_local_dependencies %} - {% for dependency in system_test_local_dependencies %}session.install("-e", "{{dependency}}") + {% for dependency in system_test_local_dependencies %}session.install("-e", "{{dependency}}", "-c", constraints_path) {% endfor %} {%- endif %} {%- if system_test_extras_by_python %} @@ -168,11 +177,11 @@ def system(session): extras = "[{{",".join(system_test_extras_by_python[extras_python])}}]" {% endfor %}else: extras = "{%- if system_test_extras %}[{{",".join(system_test_extras)}}]{% endif %}" - session.install("-e", f".{extras}") + session.install("-e", f".{extras}", "-c", constraints_path) {% elif system_test_extras %} - session.install("-e", ".[{{",".join(system_test_extras)}}]") + session.install("-e", ".[{{",".join(system_test_extras)}}]", "-c", constraints_path) {% else %} - session.install("-e", ".") + session.install("-e", ".", "-c", constraints_path) {% endif %} # Run py.test against the system tests. diff --git a/tests/test_python_library.py b/tests/test_python_library.py index a7a22368c..fdcef75a8 100644 --- a/tests/test_python_library.py +++ b/tests/test_python_library.py @@ -27,35 +27,35 @@ @pytest.mark.parametrize( ["template_kwargs", "expected_text"], [ - ({}, ["import nox", 'session.install("-e", ".")']), + ({}, ["import nox", 'session.install("-e", ".", "-c", constraints_path)']), ( {"unit_test_local_dependencies": ["../testutils", "../unitutils"]}, [ - 'session.install("-e", "../testutils")', - 'session.install("-e", "../unitutils")', + 'session.install("-e", "../testutils", "-c", constraints_path)', + 'session.install("-e", "../unitutils", "-c", constraints_path)', ], ), ( {"system_test_local_dependencies": ["../testutils", "../sysutils"]}, [ - 'session.install("-e", "../testutils")', - 'session.install("-e", "../sysutils")', + 'session.install("-e", "../testutils", "-c", constraints_path)', + 'session.install("-e", "../sysutils", "-c", constraints_path)', ], ), ( {"unit_test_extras": ["abc", "def"]}, - ['session.install("-e", ".[abc,def]")'], + ['session.install("-e", ".[abc,def]", "-c", constraints_path)'], ), ( {"system_test_extras": ["abc", "def"]}, - ['session.install("-e", ".[abc,def]")'], + ['session.install("-e", ".[abc,def]", "-c", constraints_path)'], ), ( {"unit_test_extras_by_python": {"3.8": ["abc", "def"]}}, [ 'if session.python == "3.8":\n extras = "[abc,def]"', 'else:\n extras = ""', - 'session.install("-e", f".{extras}")', + 'session.install("-e", f".{extras}", "-c", constraints_path)', ], ), ( @@ -63,7 +63,7 @@ [ 'if session.python == "3.8":\n extras = "[abc,def]"', 'else:\n extras = ""', - 'session.install("-e", f".{extras}")', + 'session.install("-e", f".{extras}", "-c", constraints_path)', ], ), ( @@ -74,7 +74,7 @@ [ 'if session.python == "3.8":\n extras = "[abc,def]"', 'else:\n extras = "[tuv,wxyz]"', - 'session.install("-e", f".{extras}")', + 'session.install("-e", f".{extras}", "-c", constraints_path)', ], ), ( @@ -85,7 +85,7 @@ [ 'if session.python == "3.8":\n extras = "[abc,def]"', 'else:\n extras = "[tuv,wxyz]"', - 'session.install("-e", f".{extras}")', + 'session.install("-e", f".{extras}", "-c", constraints_path)', ], ), ],