diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 06f25474b..1d0e22dd5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -4,15 +4,10 @@ on: push: branches: - "main" - - "maintenance/.*" pull_request: branches: - "main" - - "maintenance/.*" schedule: - # Nightly tests run on main by default: - # Scheduled workflows run on the latest commit on the default or base branch. - # (from https://help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule) - cron: "21 0 * * *" defaults: @@ -41,7 +36,6 @@ jobs: env: OE_LICENSE: ${{ github.workspace }}/oe_license.txt - PACKAGE: openff PYTEST_ARGS: -r fE --tb=short -nauto COV: --cov=openff/toolkit --cov-append --cov-report=xml @@ -149,7 +143,7 @@ jobs: PYTEST_ARGS+=" --ignore=openff/toolkit/_tests/test_examples.py" PYTEST_ARGS+=" --ignore=openff/toolkit/_tests/test_links.py" if [[ "$GITHUB_EVENT_NAME" == "schedule" ]]; then - PYTEST_ARGS+=" --runslow" + PYTEST_ARGS+=" -m 'slow or not slow'" fi python -m pytest --durations=20 $PYTEST_ARGS $COV @@ -160,7 +154,7 @@ jobs: - name: Run notebooks in docs if: ${{ matrix.rdkit == true && matrix.openeye == true }} - run: pytest -v --no-cov --nbval --ignore docs/_build/ docs/ + run: python -m pytest -v --no-cov --nbval-lax docs/ - name: Run examples in docstrings if: ${{ matrix.rdkit == true && matrix.openeye == true }} diff --git a/.github/workflows/beta_rc.yaml b/.github/workflows/beta_rc.yaml index 84e157bd5..6e42be5c8 100644 --- a/.github/workflows/beta_rc.yaml +++ b/.github/workflows/beta_rc.yaml @@ -78,7 +78,7 @@ jobs: run: | PYTEST_ARGS+=" --ignore=openff/toolkit/_tests/test_examples.py" PYTEST_ARGS+=" --ignore=openff/toolkit/_tests/test_links.py" - PYTEST_ARGS+=" --runslow" + PYTEST_ARGS+=" -m 'slow or not slow'" pytest $PYTEST_ARGS - name: Run code snippets in docs diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 6d5245c62..10f9d7972 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -1,14 +1,6 @@ name: Examples on: - push: - branches: - - "main" - - "maintenance/.+" - pull_request: - branches: - - "main" - - "maintenance/.+" schedule: - cron: "0 0 * * *" @@ -66,24 +58,13 @@ jobs: create-args: >- python=${{ matrix.python-version }} - - name: Additional info about the build - run: | - uname -a - df -h - ulimit -a - - name: Make oe_license.txt file from GH org secret "OE_LICENSE" env: OE_LICENSE_TEXT: ${{ secrets.OE_LICENSE }} - run: | - echo "${OE_LICENSE_TEXT}" > ${OE_LICENSE} + run: echo "${OE_LICENSE_TEXT}" > ${OE_LICENSE} - name: Install package - run: | - # Maybe remove the packaged openff-toolkit, installed as a dependency of openmmforcefields - # and/or Interchange - micromamba remove --force openff-toolkit-base - python -m pip install . + run: python -m pip install . - name: Remove undesired toolkits run: | @@ -110,6 +91,7 @@ jobs: python -c "from openff.toolkit.utils.toolkits import ${TK}_AVAILABLE; assert not ${TK}_AVAILABLE, '${TK} available'" done fi + - name: Environment Information run: | micromamba info diff --git a/devtools/conda-envs/openeye-examples.yaml b/devtools/conda-envs/openeye-examples.yaml index bf969a7e3..a8d0fc7fd 100644 --- a/devtools/conda-envs/openeye-examples.yaml +++ b/devtools/conda-envs/openeye-examples.yaml @@ -27,7 +27,7 @@ dependencies: # Toolkit-specific - openeye-toolkits # Test-only/optional/dev/typing/examples - - pytest + - pytest =8 - pytest-xdist - pytest-rerunfailures - pyyaml diff --git a/devtools/conda-envs/openeye.yaml b/devtools/conda-envs/openeye.yaml index 36f06df95..fbbb38404 100644 --- a/devtools/conda-envs/openeye.yaml +++ b/devtools/conda-envs/openeye.yaml @@ -26,7 +26,7 @@ dependencies: # Toolkit-specific - openeye-toolkits # Test-only/optional/dev/typing - - pytest + - pytest =8 - pytest-cov - pytest-xdist - pytest-rerunfailures diff --git a/devtools/conda-envs/rdkit-examples.yaml b/devtools/conda-envs/rdkit-examples.yaml index 8d3c85e28..5b20a4fb8 100644 --- a/devtools/conda-envs/rdkit-examples.yaml +++ b/devtools/conda-envs/rdkit-examples.yaml @@ -30,7 +30,7 @@ dependencies: # https://github.com/rdkit/rdkit/issues/7221 and https://github.com/rdkit/rdkit/issues/7583 - rdkit =2024 # Test-only/optional/dev/typing/examples - - pytest + - pytest =8 - pytest-xdist - pytest-rerunfailures - pyyaml diff --git a/devtools/conda-envs/rdkit.yaml b/devtools/conda-envs/rdkit.yaml index e48a8dc19..66c7e4ad6 100644 --- a/devtools/conda-envs/rdkit.yaml +++ b/devtools/conda-envs/rdkit.yaml @@ -27,7 +27,7 @@ dependencies: # https://github.com/rdkit/rdkit/issues/7221 and https://github.com/rdkit/rdkit/issues/7583 - rdkit !=2024.03.6,!=2024.03.5 # Test-only/optional/dev/typing - - pytest + - pytest =8 - pytest-cov - pytest-xdist - pytest-rerunfailures diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 3ae44cc0f..a97003a82 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -32,7 +32,7 @@ dependencies: - openeye-toolkits # Test-only/optional/dev/typing - - pytest + - pytest =8 - pytest-cov - pytest-xdist - pytest-rerunfailures diff --git a/docs/releasehistory.md b/docs/releasehistory.md index 18cf8b10e..1047349b3 100644 --- a/docs/releasehistory.md +++ b/docs/releasehistory.md @@ -28,6 +28,10 @@ Releases follow the `major.minor.micro` scheme recommended by [PEP440](https://w - [PR #1981](https://github.com/openforcefield/openff-toolkit/pull/1981): Updates documentation to run quicker and use new features, including some in Interchange 0.4. +### Miscellaneous + +- [PR #1822](https://github.com/openforcefield/openff-toolkit/pull/1922): Updates internal tests to use Pytest 8. + ## 0.16.7 ### Bugfixes diff --git a/openff/toolkit/_tests/conftest.py b/openff/toolkit/_tests/conftest.py index 6ee9dcfc8..3a09bbbd8 100644 --- a/openff/toolkit/_tests/conftest.py +++ b/openff/toolkit/_tests/conftest.py @@ -1,9 +1,5 @@ """ Configuration file for pytest. - -This adds the following command line options. -- runslow: Run tests marked as slow (default is False). - """ import logging @@ -20,16 +16,6 @@ pass -def pytest_configure(config): - """ - Initialization hook to register custom markers without a pytest.ini - More info: https://docs.pytest.org/en/latest/reference.html#initialization-hooks - """ - config.addinivalue_line( - "markers", "slow: marks tests as slow (deselect with `-m 'not slow'`)" - ) - - def untar_full_alkethoh_and_freesolv_set(): """When running slow tests, we unpack the full AlkEthOH and FreeSolv test sets in advance to speed things up. diff --git a/openff/toolkit/_tests/test_examples.py b/openff/toolkit/_tests/test_examples.py index 221335b0e..71a9124d0 100644 --- a/openff/toolkit/_tests/test_examples.py +++ b/openff/toolkit/_tests/test_examples.py @@ -36,8 +36,7 @@ def run_script_str(script_str): """ with tempfile.TemporaryDirectory() as tmp_dir: - temp_file_path = pathlib.Path(tmp_dir, "temp.py").as_posix() - + temp_file_path = (pathlib.Path(tmp_dir) / "temp.py").as_posix() # Create temporary python script. with open(temp_file_path, "w") as f: f.write(script_str) @@ -57,16 +56,17 @@ def find_example_scripts() -> list[str]: example_file_paths : list[str] List of full paths to python scripts to execute. """ - # Count on the examples/ path being equivalently accessible as the README file - readme_file_path = _get_readme_path() - - if readme_file_path is None: + if "site-packages" in __file__: + # This test file is being collected from the installed package, which + # does not provide the examples folder in the same location return list() - examples_dir_path = pathlib.Path(_get_readme_path().parent, "examples") + examples_dir_path = pathlib.Path(__file__).parents[3] / "examples" # Examples that require RDKit - rdkit_examples = {examples_dir_path / "conformer_energies/conformer_energies.py"} + rdkit_examples = { + examples_dir_path / "conformer_energies/conformer_energies.py", + } example_file_paths = [] for example_file_path in examples_dir_path.glob("*/*.py"): diff --git a/openff/toolkit/_tests/test_forcefield.py b/openff/toolkit/_tests/test_forcefield.py index 02c40326e..0f75eea66 100644 --- a/openff/toolkit/_tests/test_forcefield.py +++ b/openff/toolkit/_tests/test_forcefield.py @@ -1325,7 +1325,7 @@ def test_parameterize_large_system( force_field, ): """Test parameterizing a large system of several distinct molecules. - This test is very slow, so it is only run if the --runslow option is provided to pytest. + This test is very slow, so it is only run if the slow marker option is provided to pytest. """ box_file_path = get_data_file_path( os.path.join("systems", "packmol_boxes", box) @@ -1882,9 +1882,8 @@ def test_handlers_tracked_if_already_loaded(self): plugins = load_handler_plugins() - assert ( - len(plugins) > 0 - ), "Test assumes that some ParameterHandler plugins are available" + if len(plugins) == 0: + pytest.skip("Test assumes that some ParameterHandler plugins are available") assert ForceField(load_plugins=False)._plugin_parameter_handler_classes == [] assert ForceField(load_plugins=True)._plugin_parameter_handler_classes == [ diff --git a/openff/toolkit/_tests/test_links.py b/openff/toolkit/_tests/test_links.py index bd994c11c..b61a2c5c1 100644 --- a/openff/toolkit/_tests/test_links.py +++ b/openff/toolkit/_tests/test_links.py @@ -1,10 +1,9 @@ +import pathlib import re from urllib.request import Request, urlopen import pytest -from openff.toolkit._tests.utils import _get_readme_path - def find_readme_links() -> list[str]: """Yield all the links in the main README.md file. @@ -14,16 +13,23 @@ def find_readme_links() -> list[str]: readme_examples : list[str] The list of links included in the README.md file. """ - readme_file_path = _get_readme_path() - - if readme_file_path is None: + if "site-packages" in __file__: + # This test file is being collected from the installed package, which + # does not provide the README file. + # Note that there will likely be a mis-bundled file + # $CONDA_PREFIX/lib/python3.x/site-packages/README.md, but this is not + # the toolkit's README file! return list() else: + readme_file_path = pathlib.Path(__file__).parents[3] / "README.md" with open(readme_file_path.as_posix()) as f: readme_content = f.read() - return re.findall("http[s]?://(?:[0-9a-zA-Z]|[-/.%:_])+", readme_content) + with open(readme_file_path.as_posix()) as f: + readme_content = f.read() + + return re.findall("http[s]?://(?:[0-9a-zA-Z]|[-/.%:_])+", readme_content) @pytest.mark.parametrize("readme_link", find_readme_links()) diff --git a/openff/toolkit/_tests/test_toolkit_io.py b/openff/toolkit/_tests/test_toolkit_io.py index fe20df0ce..97dc43186 100644 --- a/openff/toolkit/_tests/test_toolkit_io.py +++ b/openff/toolkit/_tests/test_toolkit_io.py @@ -3,7 +3,6 @@ """ -import os import pathlib import sys import tempfile @@ -944,14 +943,6 @@ def test_from_file_obj_smi_supports_stringio(self): assert mol.name == "CHEMBL113" -@pytest.fixture(scope="class") -def tmpdir(request): - request.cls.tmpdir = tmpdir = tempfile.TemporaryDirectory() - with tmpdir: - yield - request.cls.tmpdir = None - - def assert_is_ethanol_sdf(f): assert f.readline() == "ethanol\n" # title line f.readline() # ignore next two lines @@ -973,12 +964,9 @@ def assert_is_ethanol_smiles(smiles): class BaseToFileIO: - def get_tmpfile(self, name): - return os.path.join(self.tmpdir.name, name) - @pytest.mark.parametrize("format_name", ["SDF", "sdf", "sDf", "mol", "MOL"]) - def test_to_file_sdf(self, format_name): - filename = self.get_tmpfile("abc.xyz") + def test_to_file_sdf(self, format_name, tmp_path): + filename = tmp_path / "abc.xyz" self.toolkit_wrapper.to_file(ETHANOL, filename, format_name) with open(filename) as f: assert_is_ethanol_sdf(f) @@ -999,8 +987,8 @@ def test_to_file_obj_sdf_with_bytesio(self): self.toolkit_wrapper.to_file_obj(ETHANOL, f, "sdf") @pytest.mark.parametrize("format_name", ["SMI", "smi", "sMi"]) - def test_to_file_smi(self, format_name): - filename = self.get_tmpfile("abc.xyz") + def test_to_file_smi(self, format_name, tmp_path): + filename = tmp_path / "abc.xyz" self.toolkit_wrapper.to_file(ETHANOL, filename, format_name) with open(filename) as f: assert_is_ethanol_smi(f) @@ -1026,19 +1014,19 @@ def test_to_file_qwe_format_raises_exception(self): self.toolkit_wrapper.to_file(ETHANOL, fileobj.name, "QWE") @pytest.mark.parametrize("format_name", ["smi", "sdf", "mol"]) - def test_to_file_when_the_file_does_not_exist(self, format_name): - filename = self.get_tmpfile("does/not/exist.smi") + def test_to_file_when_the_file_does_not_exist(self, format_name, tmp_path): + filename = tmp_path / "does/not/exist.smi" with pytest.raises(OSError): self.toolkit_wrapper.to_file(ETHANOL, filename, format_name) -@pytest.mark.usefixtures("init_toolkit", "tmpdir") +@pytest.mark.usefixtures("init_toolkit") @requires_openeye class TestOpenEyeToolkitToFileIO(BaseToFileIO): toolkit_wrapper_class = OpenEyeToolkitWrapper -@pytest.mark.usefixtures("init_toolkit", "tmpdir") +@pytest.mark.usefixtures("init_toolkit") @requires_rdkit class TestRDKitToolkitToFileIO(BaseToFileIO): toolkit_wrapper_class = RDKitToolkitWrapper diff --git a/openff/toolkit/topology/topology.py b/openff/toolkit/topology/topology.py index 0f09020c5..cc8b09b55 100644 --- a/openff/toolkit/topology/topology.py +++ b/openff/toolkit/topology/topology.py @@ -1688,12 +1688,7 @@ def from_pdb( (24, 23, 29, 36) (32, 31, 40, 42) >>> [*top.hierarchy_iterator("residues")] - [HierarchyElement ('A', '1', ' ', 'ACE') of iterator 'residues' containing 6 atom(s), - HierarchyElement ('A', '2', ' ', 'SER') of iterator 'residues' containing 11 atom(s), - HierarchyElement ('A', '3', ' ', 'NME') of iterator 'residues' containing 6 atom(s), - HierarchyElement ('B', '1', ' ', 'ACE') of iterator 'residues' containing 6 atom(s), - HierarchyElement ('B', '2', ' ', 'CYS') of iterator 'residues' containing 11 atom(s), - HierarchyElement ('B', '3', ' ', 'NME') of iterator 'residues' containing 6 atom(s)] + [HierarchyElement ('A', '1', ' ', 'ACE') of iterator 'residues' containing 6 atom(s), HierarchyElement ('A', '2', ' ', 'SER') of iterator 'residues' containing 11 atom(s), HierarchyElement ('A', '3', ' ', 'NME') of iterator 'residues' containing 6 atom(s), HierarchyElement ('B', '1', ' ', 'ACE') of iterator 'residues' containing 6 atom(s), HierarchyElement ('B', '2', ' ', 'CYS') of iterator 'residues' containing 11 atom(s), HierarchyElement ('B', '3', ' ', 'NME') of iterator 'residues' containing 6 atom(s)] Polymer systems can also be supported if ``_custom_substructures`` are given as a ``dict[str, list[str]]``, where the keys are unique atom @@ -1716,7 +1711,7 @@ def from_pdb( ... get_data_file_path("systems/test_systems/PE.pdb"), ... _custom_substructures=PE_substructs, ... ) - """ + """ # noqa: E501 import io import json diff --git a/openff/toolkit/typing/engines/smirnoff/parameters.py b/openff/toolkit/typing/engines/smirnoff/parameters.py index 24a2637d0..7440588bc 100644 --- a/openff/toolkit/typing/engines/smirnoff/parameters.py +++ b/openff/toolkit/typing/engines/smirnoff/parameters.py @@ -291,11 +291,10 @@ class ParameterAttribute: >>> my_par.attr_quantity = '1.0 * nanometer' >>> my_par.attr_quantity - >>> my_par.attr_quantity = 3.0 + >>> my_par.attr_quantity = 3.0 # doctest: +ELLIPSIS Traceback (most recent call last): ... - openff.toolkit.utils.exceptions.IncompatibleUnitError: - attr_quantity=3.0 dimensionless should have units of angstrom + openff.toolkit.utils.exceptions.IncompatibleUnitError: attr_quantity=3.0 dimensionless should have units of angstrom You can attach a custom converter to an attribute. @@ -334,7 +333,7 @@ class ParameterAttribute: ... TypeError: Cannot convert '4.0' to float - """ + """ # noqa: E501 UNDEFINED = UNDEFINED """Marker type for an undeclared default parameter.""" @@ -734,21 +733,18 @@ class _ParameterAttributeHandler: While assigning incompatible units is forbidden. - >>> my_par.k = 3.0 * unit.gram + >>> my_par.k = 3.0 * unit.gram # doctest: +ELLIPSIS Traceback (most recent call last): ... - openff.toolkit.utils.exceptions.IncompatibleUnitError: - k=3.0 gram should have units of kilocalorie / angstrom ** 2 / mole + openff.toolkit.utils.exceptions.IncompatibleUnitError: k=3.0 gram should have units of kilocalorie / angstrom ** 2 / mole On top of type checking, the constructor implemented in ``_ParameterAttributeHandler`` checks if some required parameters are not given. - >>> ParameterTypeOrHandler(length=3.0*unit.nanometer) + >>> ParameterTypeOrHandler(length=3.0*unit.nanometer) # doctest: +ELLIPSIS Traceback (most recent call last): ... - openff.toolkit.utils.exceptions.SMIRNOFFSpecError: - require the following missing - parameters: ['k']. Defined kwargs are ['length'] + openff.toolkit.utils.exceptions.SMIRNOFFSpecError: require the following missing parameters: ['k']. Defined kwargs are ['length'] Each attribute can be made optional by specifying a default value, and you can attach a converter function by passing a callable as an @@ -812,7 +808,7 @@ class _ParameterAttributeHandler: >>> my_par.periodicity [1, 6] - """ + """ # noqa: E501 def __init__(self, allow_cosmetic_attributes=False, **kwargs): """ @@ -1697,11 +1693,10 @@ class ParameterType(_ParameterAttributeHandler): ... ) >>> my_par.length - >>> my_par.k = 3.0 * unit.gram + >>> my_par.k = 3.0 * unit.gram # doctest: +ELLIPSIS Traceback (most recent call last): ... - openff.toolkit.utils.exceptions.IncompatibleUnitError: - k=3.0 gram should have units of kilocalorie / angstrom ** 2 / mole + openff.toolkit.utils.exceptions.IncompatibleUnitError: k=3.0 gram should have units of kilocalorie / angstrom ** 2 / mole Each attribute can be made optional by specifying a default value, and you can attach a converter function by passing a callable as an @@ -1769,7 +1764,7 @@ class ParameterType(_ParameterAttributeHandler): >>> my_par.periodicity [1, 6] - """ + """ # noqa: E501 # The string mapping to this ParameterType in a SMIRNOFF data source _ELEMENT_NAME: Optional[str] = None diff --git a/pyproject.toml b/pyproject.toml index 5fcb645c9..a82bb81db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,14 +46,21 @@ known-third-party = ["openff.interchange", "openff.utilities", "openff.units"] known-first-party = ["openff.toolkit"] [tool.pytest.ini_options] +testpaths = [ + "openff/toolkit/_tests/", +] +markers = [ + "slow: marks tests as slow" +] +addopts = '-m "not slow"' +filterwarnings = [ + "ignore:Molecule.from_pdb_and_smiles.*deprecated in favor of .*from_pdb" +] doctest_optionflags = [ "ELLIPSIS", "DONT_ACCEPT_TRUE_FOR_1", "NORMALIZE_WHITESPACE", ] -testpaths = [ - "openff/toolkit/_tests/", -] [tool.coverage.run] omit = [