diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 58c1505..49d518b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,11 @@ Next Release Breaking changes: +- deprecate the function ``pytest_container.container_from_pytest_param``, + please use + :py:func:`~pytest_container.container.container_and_marks_from_pytest_param` + instead. + Improvements and new features: diff --git a/poetry.lock b/poetry.lock index f628115..0f82b61 100644 --- a/poetry.lock +++ b/poetry.lock @@ -206,6 +206,20 @@ files = [ {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] +[[package]] +name = "deprecation" +version = "2.1.0" +description = "A library to handle automated deprecations" +optional = false +python-versions = "*" +files = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] + +[package.dependencies] +packaging = "*" + [[package]] name = "dill" version = "0.3.4" @@ -1139,4 +1153,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black ( [metadata] lock-version = "2.0" python-versions = ">=3.6.2,<4.0" -content-hash = "df615dc6120ae8b3be7ef9c11868fe483c0a7f6600a8e5c4f3963838427d4e5c" +content-hash = "1254a784b736e00c16cf0e7d3d919dd0c5e04ccf3bfa3bd5e6d9ce8153357442" diff --git a/pyproject.toml b/pyproject.toml index 7eebccf..ab27135 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dataclasses = { version = ">=0.8", python = "< 3.7" } typing-extensions = { version = ">=3.0", markers="python_version < '3.8'" } cached-property = { version = "^1.5", markers="python_version < '3.8'" } filelock = "^3.4" +deprecation = "^2.1" [tool.poetry.dev-dependencies] black = ">=21.9b0" @@ -57,5 +58,5 @@ line-length = 79 strict = true [[tool.mypy.overrides]] -module = "testinfra" +module = "testinfra,deprecation" ignore_missing_imports = true diff --git a/pytest_container/__init__.py b/pytest_container/__init__.py index cf2d0ee..78983d1 100644 --- a/pytest_container/__init__.py +++ b/pytest_container/__init__.py @@ -5,6 +5,7 @@ from .build import GitRepositoryBuild from .build import MultiStageBuild from .container import Container +from .container import container_and_marks_from_pytest_param from .container import container_from_pytest_param from .container import container_to_pytest_param from .container import DerivedContainer diff --git a/pytest_container/build.py b/pytest_container/build.py index cde8b79..c1a2dcd 100644 --- a/pytest_container/build.py +++ b/pytest_container/build.py @@ -19,7 +19,7 @@ from _pytest.config import Config from _pytest.mark.structures import ParameterSet from pytest_container.container import Container -from pytest_container.container import container_from_pytest_param +from pytest_container.container import container_and_marks_from_pytest_param from pytest_container.container import DerivedContainer from pytest_container.logging import _logger from pytest_container.runtime import OciRuntimeBase @@ -158,7 +158,9 @@ def containerfile(self) -> str: **{ k: v if isinstance(v, str) - else str(container_from_pytest_param(v)._build_tag) + else str( + container_and_marks_from_pytest_param(v)[0]._build_tag + ) for k, v in self.containers.items() } ) @@ -178,9 +180,9 @@ def prepare_build( _logger.debug("Preparing multistage build") for _, container in self.containers.items(): if not isinstance(container, str): - container_from_pytest_param(container).prepare_container( - rootdir, extra_build_args - ) + container_and_marks_from_pytest_param(container)[ + 0 + ].prepare_container(rootdir, extra_build_args) dockerfile_dest = tmp_path / "Dockerfile" with open(dockerfile_dest, "w", encoding="utf-8") as containerfile: diff --git a/pytest_container/container.py b/pytest_container/container.py index aa4a3a0..b0e98da 100644 --- a/pytest_container/container.py +++ b/pytest_container/container.py @@ -34,14 +34,15 @@ from typing import List from typing import Optional from typing import overload +from typing import Tuple from typing import Type from typing import Union from uuid import uuid4 +import deprecation import pytest import testinfra -from _pytest.mark.structures import MarkDecorator -from _pytest.mark.structures import ParameterSet +from _pytest.mark import ParameterSet from filelock import FileLock from pytest_container.inspect import ContainerHealth from pytest_container.inspect import ContainerInspect @@ -51,6 +52,13 @@ from pytest_container.runtime import get_selected_runtime from pytest_container.runtime import OciRuntimeBase +if sys.version_info >= (3, 8): + from importlib import metadata + from typing import Literal +else: + import importlib_metadata as metadata + from typing_extensions import Literal + @enum.unique class ImageFormat(enum.Enum): @@ -853,7 +861,9 @@ def inspect(self) -> ContainerInspect: def container_to_pytest_param( container: ContainerBase, - marks: Optional[Union[Collection[MarkDecorator], MarkDecorator]] = None, + marks: Optional[ + Union[Collection[pytest.MarkDecorator], pytest.MarkDecorator] + ] = None, ) -> ParameterSet: """Converts a subclass of :py:class:`~pytest_container.container.ContainerBase` (:py:class:`~pytest_container.container.Container` or @@ -869,6 +879,64 @@ def container_to_pytest_param( return pytest.param(container, marks=marks or [], id=str(container)) +@overload +def container_and_marks_from_pytest_param( + param: Container, +) -> Tuple[Container, Literal[None]]: + ... + + +@overload +def container_and_marks_from_pytest_param( + param: DerivedContainer, +) -> Tuple[DerivedContainer, Literal[None]]: + ... + + +@overload +def container_and_marks_from_pytest_param( + param: ParameterSet, +) -> Tuple[ + Union[Container, DerivedContainer], + Optional[Collection[Union[pytest.MarkDecorator, pytest.Mark]]], +]: + ... + + +def container_and_marks_from_pytest_param( + param: Union[ParameterSet, Container, DerivedContainer], +) -> Tuple[ + Union[Container, DerivedContainer], + Optional[Collection[Union[pytest.MarkDecorator, pytest.Mark]]], +]: + """Extracts the :py:class:`~pytest_container.container.Container` or + :py:class:`~pytest_container.container.DerivedContainer` and the + corresponding marks from a `pytest.param + `_ and + returns both. + + If ``param`` is either a :py:class:`~pytest_container.container.Container` + or a :py:class:`~pytest_container.container.DerivedContainer`, then param is + returned directly and the second return value is ``None``. + + """ + if isinstance(param, (Container, DerivedContainer)): + return param, None + + if len(param.values) > 0 and isinstance( + param.values[0], (Container, DerivedContainer) + ): + return param.values[0], param.marks + + raise ValueError(f"Invalid pytest.param values: {param.values}") + + +@deprecation.deprecated( + deprecated_in="0.4.0", + removed_in="0.5.0", + current_version=metadata.version("pytest_container"), + details="use container_and_marks_from_pytest_param instead", +) # type: ignore def container_from_pytest_param( param: Union[ParameterSet, Container, DerivedContainer], ) -> Union[Container, DerivedContainer]: diff --git a/pytest_container/plugin.py b/pytest_container/plugin.py index 6cd6c41..0ad840b 100644 --- a/pytest_container/plugin.py +++ b/pytest_container/plugin.py @@ -8,7 +8,7 @@ from typing import Callable from typing import Generator -from pytest_container.container import container_from_pytest_param +from pytest_container.container import container_and_marks_from_pytest_param from pytest_container.container import ContainerData from pytest_container.container import ContainerLauncher from pytest_container.helpers import get_extra_build_args @@ -76,12 +76,12 @@ def fixture( pytest_generate_tests. """ - launch_data = container_from_pytest_param(request.param) - _logger.debug("Requesting the container %s", str(launch_data)) + container, _ = container_and_marks_from_pytest_param(request.param) + _logger.debug("Requesting the container %s", str(container)) - if scope == "session" and launch_data.singleton: + if scope == "session" and container.singleton: raise RuntimeError( - f"A singleton container ({launch_data}) cannot be used in a session level fixture" + f"A singleton container ({container}) cannot be used in a session level fixture" ) add_labels = [ @@ -100,7 +100,7 @@ def fixture( pass with ContainerLauncher( - container=launch_data, + container=container, container_runtime=container_runtime, rootdir=pytestconfig.rootpath, extra_build_args=get_extra_build_args(pytestconfig), diff --git a/tests/test_pytest_param.py b/tests/test_pytest_param.py index 11de706..bc292d6 100644 --- a/tests/test_pytest_param.py +++ b/tests/test_pytest_param.py @@ -10,6 +10,7 @@ from pytest_container import get_extra_build_args from pytest_container import MultiStageBuild from pytest_container import OciRuntimeBase +from pytest_container.container import container_and_marks_from_pytest_param from pytest_container.container import ContainerData from pytest_container.pod import Pod from pytest_container.pod import pod_from_pytest_param @@ -117,6 +118,33 @@ def test_container_from_pytest_param() -> None: assert "(16, 45)" in str(val_err_ctx.value) +def test_container_and_marks_from_pytest_param() -> None: + cont, marks = container_and_marks_from_pytest_param( + container_to_pytest_param(LEAP) + ) + assert cont == LEAP and not marks + + cont, marks = container_and_marks_from_pytest_param( + pytest.param(LEAP, 1, "a") + ) + assert cont == LEAP and not marks + + assert container_and_marks_from_pytest_param(LEAP) == (LEAP, None) + + derived = DerivedContainer(base=LEAP, containerfile="ENV foo=bar") + cont, marks = container_and_marks_from_pytest_param( + container_to_pytest_param(derived) + ) + assert cont == derived and not marks + + assert container_and_marks_from_pytest_param(derived) == (derived, None) + + with pytest.raises(ValueError) as val_err_ctx: + container_and_marks_from_pytest_param(pytest.param(16, 45)) + assert "Invalid pytest.param values" in str(val_err_ctx.value) + assert "(16, 45)" in str(val_err_ctx.value) + + @pytest.mark.parametrize( "param,expected_pod", [(TEST_POD, TEST_POD), (pytest.param(TEST_POD), TEST_POD)],