Skip to content

Commit 1031d79

Browse files
committed
Make Container inherit from ParameterSet
1 parent 3aad600 commit 1031d79

File tree

7 files changed

+317
-51
lines changed

7 files changed

+317
-51
lines changed

poetry.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pytest-testinfra = [
3535
{ version = ">=8.0", python = ">= 3.8" }
3636
]
3737
dataclasses = { version = ">=0.8", python = "< 3.7" }
38-
typing-extensions = { version = ">=3.0", markers="python_version < '3.8'" }
38+
typing-extensions = { version = ">=3.0", markers="python_version < '3.10'" }
3939
cached-property = { version = "^1.5", markers="python_version < '3.8'" }
4040
filelock = "^3.4"
4141
deprecation = "^2.1"
@@ -69,3 +69,12 @@ strict = true
6969
[[tool.mypy.overrides]]
7070
module = "testinfra,deprecation"
7171
ignore_missing_imports = true
72+
73+
[tool.pytest.ini_options]
74+
xfail_strict = true
75+
addopts = "--strict-markers"
76+
markers = [
77+
'secretleapmark',
78+
'othersecretmark',
79+
'secretpodmark',
80+
]

pytest_container/container.py

Lines changed: 134 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
from typing import List
3535
from typing import Optional
3636
from typing import overload
37+
38+
try:
39+
from typing import Self
40+
except ImportError:
41+
from typing_extensions import Self
3742
from typing import Tuple
3843
from typing import Type
3944
from typing import Union
@@ -45,6 +50,8 @@
4550
import testinfra
4651
from filelock import BaseFileLock
4752
from filelock import FileLock
53+
from pytest import Mark
54+
from pytest import MarkDecorator
4855
from pytest_container.helpers import get_always_pull_option
4956
from pytest_container.helpers import get_extra_build_args
5057
from pytest_container.helpers import get_extra_run_args
@@ -431,37 +438,46 @@ class EntrypointSelection(enum.Enum):
431438
IMAGE = enum.auto()
432439

433440

434-
@dataclass
435-
class ContainerBase:
441+
class ContainerBase(ABC, _pytest.mark.ParameterSet):
436442
"""Base class for defining containers to be tested. Not to be used directly,
437443
instead use :py:class:`Container` or :py:class:`DerivedContainer`.
438444
439445
"""
440446

447+
def __new__(cls, *args, **kwargs):
448+
# Filter out all fields of ParameterSet and invoke object.__new__ only for the
449+
# fields that it supports
450+
parameter_set_fields = _pytest.mark.ParameterSet._fields
451+
filtered_kwargs = {}
452+
for f in parameter_set_fields:
453+
filtered_kwargs[f] = kwargs.get(f, None)
454+
455+
return super().__new__(cls, *args, **filtered_kwargs)
456+
441457
#: Full url to this container via which it can be pulled
442458
#:
443459
#: If your container image is not available via a registry and only locally,
444460
#: then you can use the following syntax: ``containers-storage:$local_name``
445-
url: str = ""
461+
# url: str = ""
446462

447463
#: id of the container if it is not available via a registry URL
448-
container_id: str = ""
464+
# container_id: str = ""
449465

450466
#: Defines which entrypoint of the container is used.
451467
#: By default either :py:attr:`custom_entry_point` will be used (if defined)
452468
#: or the container's entrypoint or cmd. If neither of the two is set, then
453469
#: :file:`/bin/bash` will be used.
454-
entry_point: EntrypointSelection = EntrypointSelection.AUTO
470+
# entry_point: EntrypointSelection = EntrypointSelection.AUTO
455471

456472
#: custom entry point for this container (i.e. neither its default, nor
457473
#: :file:`/bin/bash`)
458-
custom_entry_point: Optional[str] = None
474+
# custom_entry_point: Optional[str] = None
459475

460476
#: List of additional flags that will be inserted after
461477
#: `docker/podman run -d` and before the image name (i.e. these arguments
462478
#: are not passed to the entrypoint or ``CMD``). The list must be properly
463479
#: escaped, e.g. as created by ``shlex.split``.
464-
extra_launch_args: List[str] = field(default_factory=list)
480+
# extra_launch_args: List[str] = field(default_factory=list)
465481

466482
#: List of additional arguments that are passed to the ``CMD`` or
467483
#: entrypoint. These arguments are inserted after the :command:`docker/podman
@@ -471,44 +487,96 @@ class ContainerBase:
471487
#: The arguments must not cause the container to exit early. It must remain
472488
#: active in the background, otherwise this library will not function
473489
#: properly.
474-
extra_entrypoint_args: List[str] = field(default_factory=list)
490+
# extra_entrypoint_args: List[str] = field(default_factory=list)
475491

476492
#: Time for the container to become healthy (the timeout is ignored
477493
#: when the container image defines no ``HEALTHCHECK`` or when the timeout
478494
#: is below zero).
479495
#: When the value is ``None``, then the timeout will be inferred from the
480496
#: container image's ``HEALTHCHECK`` directive.
481-
healthcheck_timeout: Optional[timedelta] = None
497+
# healthcheck_timeout: Optional[timedelta] = None
482498

483499
#: additional environment variables that should be injected into the
484500
#: container
485-
extra_environment_variables: Optional[Dict[str, str]] = None
501+
# extra_environment_variables: Optional[Dict[str, str]] = None
486502

487503
#: Indicate whether there must never be more than one running container of
488504
#: this type at all times (e.g. because it opens a shared port).
489-
singleton: bool = False
505+
# singleton: bool = False
490506

491507
#: forwarded ports of this container
492-
forwarded_ports: List[PortForwarding] = field(default_factory=list)
508+
# forwarded_ports: List[PortForwarding] = field(default_factory=list)
493509

494510
#: optional list of volumes that should be mounted in this container
495-
volume_mounts: List[Union[ContainerVolume, BindMount]] = field(
496-
default_factory=list
497-
)
511+
# volume_mounts: List[Union[ContainerVolume, BindMount]] = field(
512+
# default_factory=list
513+
# )
498514

499-
_is_local: bool = False
515+
#: optional list of marks applied to this container image under test
516+
# _marks: Collection[Union[MarkDecorator, Mark]] = field(
517+
# default_factory=list
518+
# )
519+
520+
# _is_local: bool = False
521+
522+
def __init__(
523+
self,
524+
url: str = "",
525+
container_id: str = "",
526+
entry_point: EntrypointSelection = EntrypointSelection.AUTO,
527+
custom_entry_point: Optional[str] = None,
528+
extra_launch_args: Optional[List[str]] = None,
529+
extra_entrypoint_args: Optional[List[str]] = None,
530+
healthcheck_timeout: Optional[timedelta] = None,
531+
extra_environment_variables: Optional[Dict[str, str]] = None,
532+
singleton: bool = False,
533+
forwarded_ports: Optional[List[PortForwarding]] = None,
534+
volume_mounts: Optional[
535+
List[Union[ContainerVolume, BindMount]]
536+
] = None,
537+
_marks: Optional[Collection[Union[MarkDecorator, Mark]]] = None,
538+
) -> None:
539+
self.url = url
540+
self.container_id = container_id
541+
self.entry_point = entry_point
542+
self.custom_entry_point = custom_entry_point
543+
self.extra_launch_args = extra_launch_args or []
544+
self.extra_entrypoint_args = extra_entrypoint_args or []
545+
self.healthcheck_timeout = healthcheck_timeout
546+
self.extra_environment_variables = extra_environment_variables
547+
self.singleton = singleton
548+
self.forwarded_ports = forwarded_ports or []
549+
self.volume_mounts = volume_mounts or []
550+
self._marks = _marks or []
500551

501-
def __post_init__(self) -> None:
502552
local_prefix = "containers-storage:"
503553
if self.url.startswith(local_prefix):
504554
self._is_local = True
505555
# returns before_separator, separator, after_separator
506556
before, sep, self.url = self.url.partition(local_prefix)
507557
assert before == "" and sep == local_prefix
558+
else:
559+
self._is_local = False
560+
561+
def __eq__(self, value: object) -> bool:
562+
if not isinstance(value, ContainerBase):
563+
return False
564+
565+
if set(self.__dict__.keys()) != set(value.__dict__.keys()):
566+
return False
567+
568+
for k, v in self.__dict__.items():
569+
if v != value.__dict__[k]:
570+
return False
571+
572+
return True
508573

509574
def __str__(self) -> str:
510575
return self.url or self.container_id
511576

577+
def __bool__(self) -> bool:
578+
return True
579+
512580
@property
513581
def _build_tag(self) -> str:
514582
"""Internal build tag assigned to each immage, either the image url or
@@ -525,6 +593,18 @@ def local_image(self) -> bool:
525593
"""
526594
return self._is_local
527595

596+
@property
597+
def marks(self) -> Collection[Union[MarkDecorator, Mark]]:
598+
return self._marks
599+
600+
@property
601+
def values(self) -> Tuple[Self, ...]:
602+
return (self,)
603+
604+
@property
605+
def id(self) -> str:
606+
return str(self)
607+
528608
def get_launch_cmd(
529609
self,
530610
container_runtime: OciRuntimeBase,
@@ -630,13 +710,6 @@ def filelock_filename(self) -> str:
630710
# that is not available on old python versions that we still support
631711
return f"{sha3_256((''.join(all_elements)).encode()).hexdigest()}.lock"
632712

633-
634-
class ContainerBaseABC(ABC):
635-
"""Abstract base class defining the methods that must be implemented by the
636-
classes fed to the ``*container*`` fixtures.
637-
638-
"""
639-
640713
@abstractmethod
641714
def prepare_container(
642715
self,
@@ -662,8 +735,7 @@ def baseurl(self) -> Optional[str]:
662735
"""
663736

664737

665-
@dataclass(unsafe_hash=True)
666-
class Container(ContainerBase, ContainerBaseABC):
738+
class Container(ContainerBase):
667739
"""This class stores information about the Container Image under test."""
668740

669741
def pull_container(self, container_runtime: OciRuntimeBase) -> None:
@@ -702,19 +774,34 @@ def baseurl(self) -> Optional[str]:
702774

703775

704776
@dataclass(unsafe_hash=True)
705-
class DerivedContainer(ContainerBase, ContainerBaseABC):
777+
class DerivedContainer(ContainerBase):
706778
"""Class for storing information about the Container Image under test, that
707779
is build from a :file:`Containerfile`/:file:`Dockerfile` from a different
708780
image (can be any image from a registry or an instance of
709781
:py:class:`Container` or :py:class:`DerivedContainer`).
710782
711783
"""
712784

713-
base: Union[Container, "DerivedContainer", str] = ""
785+
def __init__(
786+
self,
787+
base: Union[Container, "DerivedContainer", str],
788+
containerfile: str = "",
789+
image_format: Optional[ImageFormat] = None,
790+
add_build_tags: Optional[List[str]] = None,
791+
*args,
792+
**kwargs,
793+
) -> None:
794+
super().__init__(*args, **kwargs)
795+
self.base = base
796+
self.containerfile = containerfile
797+
self.image_format = image_format
798+
self.add_build_tags = add_build_tags or []
799+
800+
# base: Union[Container, "DerivedContainer", str] = ""
714801

715802
#: The :file:`Containerfile` that is used to build this container derived
716803
#: from :py:attr:`base`.
717-
containerfile: str = ""
804+
# containerfile: str = ""
718805

719806
#: An optional image format when building images with :command:`buildah`. It
720807
#: is ignored when the container runtime is :command:`docker`.
@@ -723,16 +810,28 @@ class DerivedContainer(ContainerBase, ContainerBaseABC):
723810
#: ``docker`` image format will be used instead.
724811
#: Specifying an image format disables the auto-detection and uses the
725812
#: supplied value.
726-
image_format: Optional[ImageFormat] = None
813+
# image_format: Optional[ImageFormat] = None
727814

728815
#: Additional build tags/names that should be added to the container once it
729816
#: has been built
730-
add_build_tags: List[str] = field(default_factory=list)
817+
# add_build_tags: List[str] = field(default_factory=list)
731818

732-
def __post_init__(self) -> None:
733-
super().__post_init__()
734-
if not self.base:
735-
raise ValueError("A base container must be provided")
819+
@staticmethod
820+
def _get_recursive_marks(
821+
ctr: Union[Container, "DerivedContainer", str]
822+
) -> Collection[Union[MarkDecorator, Mark]]:
823+
if isinstance(ctr, str):
824+
return []
825+
if isinstance(ctr, Container):
826+
return ctr._marks
827+
828+
return tuple(ctr._marks) + tuple(
829+
DerivedContainer._get_recursive_marks(ctr.base)
830+
)
831+
832+
@property
833+
def marks(self) -> Collection[Union[MarkDecorator, Mark]]:
834+
return DerivedContainer._get_recursive_marks(self)
736835

737836
@property
738837
def baseurl(self) -> Optional[str]:

pytest_container/plugin.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@
77
from subprocess import run
88
from typing import Callable
99
from typing import Generator
10+
from typing import Union
1011

12+
from pytest_container.container import Container
1113
from pytest_container.container import container_and_marks_from_pytest_param
1214
from pytest_container.container import ContainerData
1315
from pytest_container.container import ContainerLauncher
16+
from pytest_container.container import DerivedContainer
17+
from pytest_container.helpers import get_extra_build_args
18+
from pytest_container.helpers import get_extra_pod_create_args
19+
from pytest_container.helpers import get_extra_run_args
1420
from pytest_container.logging import _logger
1521
from pytest_container.pod import pod_from_pytest_param
1622
from pytest_container.pod import PodData
@@ -74,13 +80,12 @@ def fixture_funct(
7480
pytest_generate_tests.
7581
"""
7682

77-
try:
78-
container, _ = container_and_marks_from_pytest_param(request.param)
79-
except AttributeError as attr_err:
80-
raise RuntimeError(
81-
"This fixture was not parametrized correctly, "
82-
"did you forget to call `auto_container_parametrize` in `pytest_generate_tests`?"
83-
) from attr_err
83+
container: Union[DerivedContainer, Container] = (
84+
request.param
85+
if isinstance(request.param, (DerivedContainer, Container))
86+
else request.param[0]
87+
)
88+
assert isinstance(container, (DerivedContainer, Container))
8489
_logger.debug("Requesting the container %s", str(container))
8590

8691
if scope == "session" and container.singleton:

0 commit comments

Comments
 (0)