Skip to content

Commit 10b15e0

Browse files
authored
Fix cirq-core dependencies (#4369)
Some of the cirq-XXX modules were lacking the dependency on cirq-core. This PR adds tests in isolation for each of the modules where they are installed and pytested in a clean environment. Fixes #4361.
1 parent 8c505fb commit 10b15e0

23 files changed

+258
-86
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,19 +130,19 @@ jobs:
130130
run: pip install -r dev_tools/requirements/deps/tensorflow-docs.txt
131131
- name: Doc check
132132
run: check/nbformat
133-
cirq-only:
134-
name: Pytest (cirq-only) Ubuntu
133+
isolated-modules:
134+
name: Isolated pytest Ubuntu
135135
runs-on: ubuntu-20.04
136136
steps:
137137
- uses: actions/checkout@v2
138138
- uses: actions/setup-python@v1
139139
with:
140140
python-version: '3.8'
141141
architecture: 'x64'
142-
- name: Install requirements
143-
run: pip install -r dev_tools/requirements/cirq-only.env.txt
144-
- name: Pytest check
145-
run: check/pytest --cirq-only --ignore=cirq-core/cirq/contrib --actually-quiet
142+
- name: Install dependencies
143+
run: pip install -r dev_tools/requirements/isolated-base.env.txt
144+
- name: Test each module in isolation
145+
run: pytest -n auto -m slow dev_tools/packaging/isolated_packages_test.py
146146
pytest:
147147
name: Pytest Ubuntu
148148
strategy:
@@ -267,7 +267,7 @@ jobs:
267267
python-version: '3.8'
268268
architecture: 'x64'
269269
- name: Install requirements
270-
run: pip install -r dev_tools/requirements/isolated-notebooks.env.txt
270+
run: pip install -r dev_tools/requirements/isolated-base.env.txt
271271
- name: Notebook tests
272272
run: check/pytest -n auto -m slow dev_tools/notebooks/isolated_notebook_test.py -k ${{matrix.partition}}
273273
- uses: actions/upload-artifact@v2

check/pytest

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
# Runs pytest on the repository.
55
#
66
# Usage:
7-
# check/pytest [--actually-quiet] [--cirq-only] [--flags for pytest] [file-paths-relative-to-repo-root]
7+
# check/pytest [--actually-quiet] [--flags for pytest] [file-paths-relative-to-repo-root]
88
#
99
# The --actually-quiet argument filters out any progress output from pytest.
10-
# If --cirq-only is specified, only cirq-core tests are executed other cirq modules won't be available on the
11-
# PYTHONPATH, which is useful to test cirq-core's ability to function independently.
1210
#
1311
# You may specify pytest flags and specific files to test. The file paths
1412
# must be relative to the repository root. If no files are specified, everything
@@ -21,24 +19,15 @@ cd "$(git rev-parse --show-toplevel)"
2119

2220
PYTEST_ARGS=()
2321
ACTUALLY_QUIET=""
24-
CIRQ_ONLY=""
2522
for arg in $@; do
2623
if [[ "${arg}" == "--actually-quiet" ]]; then
2724
ACTUALLY_QUIET=1
28-
elif [[ "${arg}" == "--cirq-only" ]]; then
29-
CIRQ_ONLY=1
3025
else
3126
PYTEST_ARGS+=("${arg}")
3227
fi
3328
done
3429

35-
if [ -z "${CIRQ_ONLY}" ]; then
36-
source dev_tools/pypath
37-
else
38-
export PYTHONPATH=cirq-core
39-
PYTEST_ARGS+=("./cirq-core")
40-
fi
41-
30+
source dev_tools/pypath
4231
PYTHON_VERSION=$(python -V 2>&1 | sed 's/.* \([0-9]\).\([0-9]\).*/\1\2/')
4332
if [ "$PYTHON_VERSION" -lt "37" ]; then
4433
PYTEST_ARGS+=("--ignore=cirq-rigetti")

cirq-aqt/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
# Read in requirements
4646
requirements = open('requirements.txt').readlines()
4747
requirements = [r.strip() for r in requirements]
48+
requirements += [f'cirq-core=={__version__}']
4849

4950
cirq_packages = ['cirq_aqt'] + [
5051
'cirq_aqt.' + package for package in find_packages(where='cirq_aqt')

cirq-ionq/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
requests~=2.18

cirq-ionq/setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
# Sanity check
5252
assert __version__, 'Version string cannot be empty'
5353

54+
requirements += [f'cirq-core=={__version__}']
55+
5456
setup(
5557
name=name,
5658
version=__version__,

cirq-pasqal/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
requests~=2.18

cirq-pasqal/setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,15 @@
4343
# Read in requirements
4444
requirements = open('requirements.txt').readlines()
4545
requirements = [r.strip() for r in requirements]
46+
# Sanity check
47+
assert __version__, 'Version string cannot be empty'
48+
49+
requirements += [f'cirq-core=={__version__}']
4650

4751
cirq_packages = ['cirq_pasqal'] + [
4852
'cirq_pasqal.' + package for package in find_packages(where='cirq_pasqal')
4953
]
5054

51-
# Sanity check
52-
assert __version__, 'Version string cannot be empty'
53-
5455
setup(
5556
name=name,
5657
version=__version__,

cirq-rigetti/setup.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,15 @@
4444
requirements = open('requirements.txt').readlines()
4545
requirements = [r.strip() for r in requirements]
4646

47+
# Sanity check
48+
assert __version__, 'Version string cannot be empty'
49+
50+
requirements += [f'cirq-core=={__version__}']
51+
4752
cirq_packages = ['cirq_rigetti'] + [
4853
'cirq_rigetti.' + package for package in find_packages(where='cirq_rigetti')
4954
]
5055

51-
# Sanity check
52-
assert __version__, 'Version string cannot be empty'
5356

5457
setup(
5558
name=name,

cirq-web/setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
# Sanity check
4747
assert __version__, 'Version string cannot be empty'
4848

49-
# This is a pure metapackage that installs all our packages
5049
requirements += [f'cirq-core=={__version__}']
5150

5251
# Gather all packages from cirq_web, and the dist/ folder from cirq_ts

dev_tools/bash_scripts_test.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,12 @@
1616
from typing import TYPE_CHECKING, Iterable
1717

1818
from dev_tools import shell_tools
19+
from dev_tools.test_utils import only_on_posix
1920

2021
if TYPE_CHECKING:
2122
import _pytest.tmpdir
2223

2324

24-
def only_on_posix(func):
25-
if os.name != 'posix':
26-
return None
27-
return func
28-
29-
3025
def run(
3126
*,
3227
script_file: str,

dev_tools/cloned_env_test.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2020 The Cirq Developers
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+
# https://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+
"""Tests the cloned_env fixture in conftest.py"""
16+
import json
17+
import os
18+
from unittest import mock
19+
20+
import pytest
21+
22+
from dev_tools import shell_tools
23+
from dev_tools.test_utils import only_on_posix
24+
25+
26+
# due to shell_tools dependencies windows builds break on this
27+
# see https://github.com/quantumlib/Cirq/issues/4394
28+
@only_on_posix
29+
# ensure that no cirq packages are on the PYTHONPATH, this is important, otherwise
30+
# the "isolation" fails and all the cirq modules would be in the list
31+
@mock.patch.dict(os.environ, {"PYTHONPATH": ""})
32+
@pytest.mark.parametrize('param', ['a', 'b', 'c'])
33+
def test_isolated_env_cloning(cloned_env, param):
34+
env = cloned_env("test_isolated", "flynt==0.64")
35+
assert (env / "bin" / "pip").is_file()
36+
37+
result = shell_tools.run_cmd(
38+
*f"{env}/bin/pip list --format=json".split(), out=shell_tools.TeeCapture()
39+
)
40+
packages = json.loads(result.out)
41+
assert {"name": "flynt", "version": "0.64"} in packages
42+
assert {"astor", "flynt", "pip", "setuptools", "wheel"} == set(p['name'] for p in packages)

dev_tools/conftest.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,19 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import os
15+
import shutil
16+
import sys
17+
import tempfile
18+
import uuid
19+
from pathlib import Path
20+
from typing import Tuple
1421

1522
import pytest
23+
from filelock import FileLock
24+
25+
from dev_tools import shell_tools
26+
from dev_tools.env_tools import create_virtual_env
1627

1728

1829
def pytest_configure(config):
@@ -29,3 +40,85 @@ def pytest_collection_modifyitems(config, items):
2940
for item in items:
3041
if 'slow' in item.keywords:
3142
item.add_marker(skip_slow_marker)
43+
44+
45+
@pytest.fixture(scope="session")
46+
def cloned_env(testrun_uid, worker_id):
47+
"""Fixture to allow tests to run in a clean virtual env.
48+
49+
It de-duplicates installation of base packages. Assuming `virtualenv-clone` exists on the PATH,
50+
it creates first a prototype environment and then clones for each new request the same env.
51+
This fixture is safe to use with parallel execution, i.e. pytest-xdist. The workers synchronize
52+
via a file lock, the first worker will (re)create the prototype environment, the others will
53+
reuse it via cloning.
54+
55+
A group of tests that share the same base environment is identified by a name, `env_dir`,
56+
which will become the directory within the temporary directory to hold the virtualenv.
57+
58+
Usage:
59+
60+
>>> def test_something_in_clean_env(cloned_env):
61+
# base_env will point to a pathlib.Path containing the virtual env which will
62+
# have quimb, jinja and whatever reqs.txt contained.
63+
base_env = cloned_env("some_tests", "quimb", "jinja", "-r", "reqs.txt")
64+
65+
# To install new packages (that are potentially different for each test instance)
66+
# just run pip install from the virtual env
67+
subprocess.run(f"{base_env}/bin/pip install something".split(" "))
68+
...
69+
70+
Returns:
71+
a function to create the cloned base environment with signature
72+
`def base_env_creator(env_dir: str, *pip_install_args: str) -> Path`.
73+
Use `env_dir` to specify the directory name per shared base packages.
74+
Use `pip_install_args` varargs to pass arguments to `pip install`, these
75+
can be requirements files, e.g. `'-r','dev_tools/.../something.txt'` or
76+
actual packages as well, e.g.`'quimb'`.
77+
"""
78+
base_dir = None
79+
80+
def base_env_creator(env_dir_name: str, *pip_install_args: str) -> Path:
81+
"""The function to create a cloned base environment."""
82+
# get/create a temp directory shared by all workers
83+
base_temp_path = Path(tempfile.gettempdir()) / "cirq-pytest"
84+
os.makedirs(name=base_temp_path, exist_ok=True)
85+
nonlocal base_dir
86+
base_dir = base_temp_path / env_dir_name
87+
with FileLock(str(base_dir) + ".lock"):
88+
if _check_for_reuse_or_recreate(base_dir):
89+
print(f"Pytest worker [{worker_id}] is reusing {base_dir} for '{env_dir_name}'.")
90+
else:
91+
print(f"Pytest worker [{worker_id}] is creating {base_dir} for '{env_dir_name}'.")
92+
_create_base_env(base_dir, pip_install_args)
93+
94+
clone_dir = base_temp_path / str(uuid.uuid4())
95+
shell_tools.run_cmd("virtualenv-clone", str(base_dir), str(clone_dir))
96+
return clone_dir
97+
98+
def _check_for_reuse_or_recreate(env_dir: Path):
99+
reuse = False
100+
if env_dir.is_dir() and (env_dir / "testrun.uid").is_file():
101+
uid = open(env_dir / "testrun.uid").readlines()[0]
102+
# if the dir is from this test session, let's reuse it
103+
if uid == testrun_uid:
104+
reuse = True
105+
else:
106+
# if we have a dir from a previous test session, recreate it
107+
shutil.rmtree(env_dir)
108+
return reuse
109+
110+
def _create_base_env(base_dir: Path, pip_install_args: Tuple[str, ...]):
111+
try:
112+
create_virtual_env(str(base_dir), [], sys.executable, True)
113+
with open(base_dir / "testrun.uid", mode="w") as f:
114+
f.write(testrun_uid)
115+
if pip_install_args:
116+
shell_tools.run_cmd(f"{base_dir}/bin/pip", "install", *pip_install_args)
117+
except BaseException as ex:
118+
# cleanup on failure
119+
if base_dir.is_dir():
120+
print(f"Removing {base_dir}, due to error: {ex}")
121+
shutil.rmtree(base_dir)
122+
raise
123+
124+
return base_env_creator

dev_tools/modules.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class Module:
5555
version: str = dataclasses.field(init=False)
5656
top_level_packages: List[str] = dataclasses.field(init=False)
5757
top_level_package_paths: List[Path] = dataclasses.field(init=False)
58+
install_requires: List[str] = dataclasses.field(init=False)
5859

5960
def __post_init__(self) -> None:
6061
self.name = self.raw_setup['name']
@@ -64,6 +65,9 @@ def __post_init__(self) -> None:
6465
self.top_level_packages = []
6566
self.top_level_package_paths = [self.root / p for p in self.top_level_packages]
6667
self.version = self.raw_setup['version']
68+
self.install_requires = (
69+
[] if 'install_requires' not in self.raw_setup else self.raw_setup['install_requires']
70+
)
6771

6872

6973
def list_modules(

dev_tools/modules_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def test_modules():
4646
assert mod1.version == '0.12.0.dev'
4747
assert mod1.top_level_packages == ['pack1']
4848
assert mod1.top_level_package_paths == [Path('mod1') / 'pack1']
49+
assert mod1.install_requires == ['req1', 'req2']
4950

5051
mod2 = Module(
5152
root=Path('mod2'), raw_setup={'name': 'module2', 'version': '1.2.3', 'packages': ['pack2']}
@@ -55,6 +56,7 @@ def test_modules():
5556
assert mod2.version == '1.2.3'
5657
assert mod2.top_level_packages == ['pack2']
5758
assert mod2.top_level_package_paths == [Path('mod2') / 'pack2']
59+
assert mod2.install_requires == []
5860
assert modules.list_modules(search_dir=Path("dev_tools/modules_test_data")) == [mod1, mod2]
5961

6062
parent = Module(

0 commit comments

Comments
 (0)