diff --git a/core/testcontainers/core/waiting_utils.py b/core/testcontainers/core/waiting_utils.py index 203de81c5..056a48192 100644 --- a/core/testcontainers/core/waiting_utils.py +++ b/core/testcontainers/core/waiting_utils.py @@ -136,7 +136,7 @@ def _poll(self, check: Callable[[], bool], transient_exceptions: Optional[list[t # Keep existing wait_container_is_ready but make it use the new system internally -def wait_container_is_ready(*transient_exceptions: type[Exception]) -> Callable[[F], F]: +def wait_container_is_ready(*transient_exceptions: type[Exception]) -> Callable[[F], F]: # noqa: C901 """ Legacy wait decorator that uses the new wait strategy system internally. Maintains backwards compatibility with existing code. diff --git a/core/tests/test_socat.py b/core/tests/test_socat.py index ded26fa29..19d4fe5ce 100644 --- a/core/tests/test_socat.py +++ b/core/tests/test_socat.py @@ -1,5 +1,5 @@ -import httpx -import pytest +import requests + from testcontainers.core.container import DockerContainer from testcontainers.core.network import Network from testcontainers.socat.socat import SocatContainer @@ -16,7 +16,7 @@ def test_socat_with_helloworld(): ): socat_url = f"http://{socat.get_container_host_ip()}:{socat.get_exposed_port(8080)}" - response = httpx.get(f"{socat_url}/ping") + response = requests.get(f"{socat_url}/ping") # noqa: S113 assert response.status_code == 200 assert response.content == b"PONG" diff --git a/doctests/conf.py b/doctests/conf.py index 0822df226..ac0e3fb4c 100644 --- a/doctests/conf.py +++ b/doctests/conf.py @@ -3,3 +3,20 @@ "sphinx.ext.doctest", ] master_doc = "README" + +doctest_global_setup = r""" +import sys +from importlib.metadata import version, PackageNotFoundError +from packaging.version import Version + +try: + _cassandra_driver_version = Version(version("cassandra-driver")) +except PackageNotFoundError: + _cassandra_driver_version = None + +SKIP_CASSANDRA_EXAMPLE = ( + _cassandra_driver_version is not None + and _cassandra_driver_version <= Version("3.29.3") + and sys.version_info > (3, 14) +) +""" diff --git a/modules/cassandra/testcontainers/cassandra/__init__.py b/modules/cassandra/testcontainers/cassandra/__init__.py index 4e6618b7b..f515aff13 100644 --- a/modules/cassandra/testcontainers/cassandra/__init__.py +++ b/modules/cassandra/testcontainers/cassandra/__init__.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. from testcontainers.core.container import DockerContainer -from testcontainers.core.waiting_utils import wait_for_logs +from testcontainers.core.wait_strategies import LogMessageWaitStrategy class CassandraContainer(DockerContainer): @@ -20,7 +20,8 @@ class CassandraContainer(DockerContainer): Example: - .. doctest:: + .. doctest:: cassandra_container + :skipif: SKIP_CASSANDRA_EXAMPLE >>> from testcontainers.cassandra import CassandraContainer >>> from cassandra.cluster import Cluster, DCAwareRoundRobinPolicy @@ -46,14 +47,7 @@ def __init__(self, image: str = "cassandra:latest", **kwargs) -> None: self.with_env("MAX_HEAP_SIZE", "1024M") self.with_env("CASSANDRA_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch") self.with_env("CASSANDRA_DC", self.DEFAULT_LOCAL_DATACENTER) - - def _connect(self): - wait_for_logs(self, "Startup complete") - - def start(self) -> "CassandraContainer": - super().start() - self._connect() - return self + self.waiting_for(LogMessageWaitStrategy("Startup complete")) def get_contact_points(self) -> list[tuple[str, int]]: return [(self.get_container_host_ip(), int(self.get_exposed_port(self.CQL_PORT)))] diff --git a/modules/cassandra/tests/test_cassandra.py b/modules/cassandra/tests/test_cassandra.py index 1aa5858b7..5be9aa81a 100644 --- a/modules/cassandra/tests/test_cassandra.py +++ b/modules/cassandra/tests/test_cassandra.py @@ -1,9 +1,18 @@ -from cassandra.cluster import Cluster, DCAwareRoundRobinPolicy - from testcontainers.cassandra import CassandraContainer +import sys + +from importlib.metadata import version +import pytest +from packaging.version import Version + +@pytest.mark.skipif( + Version(version("cassandra-driver")) <= Version("3.29.3") and sys.version_info > (3, 14), + reason="cassandra-driver <= 3.29.3 is incompatible with Python > 3.14", +) +def test_docker_run_cassandra() -> None: + from cassandra.cluster import Cluster, DCAwareRoundRobinPolicy -def test_docker_run_cassandra(): with CassandraContainer("cassandra:4.1.4") as cassandra: cluster = Cluster( cassandra.get_contact_points(), diff --git a/modules/kafka/testcontainers/kafka/__init__.py b/modules/kafka/testcontainers/kafka/__init__.py index ccd7f5b77..55683f253 100644 --- a/modules/kafka/testcontainers/kafka/__init__.py +++ b/modules/kafka/testcontainers/kafka/__init__.py @@ -1,3 +1,4 @@ +import re import tarfile import time from dataclasses import dataclass, field @@ -10,7 +11,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.utils import raise_for_deprecated_parameter from testcontainers.core.version import ComparableVersion -from testcontainers.core.waiting_utils import wait_for_logs +from testcontainers.core.wait_strategies import LogMessageWaitStrategy from testcontainers.kafka._redpanda import RedpandaContainer __all__ = [ @@ -59,7 +60,7 @@ def __init__(self, image: str = "confluentinc/cp-kafka:7.6.0", port: int = 9093, super().__init__(image, **kwargs) self.port = port self.kraft_enabled = False - self.wait_for = r".*\[KafkaServer id=\d+\] started.*" + self.wait_for: re.Pattern[str] = re.compile(r".*\[KafkaServer id=\d+\] started.*") self.boot_command = "" self.cluster_id = "MkU3OEVBNTcwNTJENDM2Qk" self.listeners = f"PLAINTEXT://0.0.0.0:{self.port},BROKER://0.0.0.0:9092" @@ -102,7 +103,7 @@ def configure(self): self._configure_zookeeper() def _configure_kraft(self) -> None: - self.wait_for = r".*Kafka Server started.*" + self.wait_for = re.compile(r".*Kafka Server started.*") self.with_env("CLUSTER_ID", self.cluster_id) self.with_env("KAFKA_NODE_ID", 1) @@ -172,14 +173,16 @@ def tc_start(self) -> None: ) self.create_file(data, KafkaContainer.TC_START_SCRIPT) - def start(self, timeout=30) -> "KafkaContainer": + def start(self, timeout: int = 30) -> "KafkaContainer": script = KafkaContainer.TC_START_SCRIPT command = f'sh -c "while [ ! -f {script} ]; do sleep 0.1; done; sh {script}"' self.configure() self.with_command(command) super().start() self.tc_start() - wait_for_logs(self, self.wait_for, timeout=timeout) + wait_strategy = LogMessageWaitStrategy(self.wait_for) + wait_strategy.with_startup_timeout(timeout) + wait_strategy.wait_until_ready(self) return self def create_file(self, content: bytes, path: str) -> None: diff --git a/modules/mysql/testcontainers/mysql/__init__.py b/modules/mysql/testcontainers/mysql/__init__.py index 4c381ff98..51026d7f9 100644 --- a/modules/mysql/testcontainers/mysql/__init__.py +++ b/modules/mysql/testcontainers/mysql/__init__.py @@ -19,7 +19,7 @@ from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter -from testcontainers.core.waiting_utils import wait_for_logs +from testcontainers.core.wait_strategies import LogMessageWaitStrategy class MySqlContainer(DbContainer): @@ -106,10 +106,10 @@ def _configure(self) -> None: self.with_env("MYSQL_PASSWORD", self.password) def _connect(self) -> None: - wait_for_logs( - self, - re.compile(".*: ready for connections.*: ready for connections.*", flags=re.DOTALL | re.MULTILINE).search, + wait_strategy = LogMessageWaitStrategy( + re.compile(r".*: ready for connections.*: ready for connections.*", flags=re.DOTALL | re.MULTILINE), ) + wait_strategy.wait_until_ready(self) def get_connection_url(self) -> str: return super()._create_connection_url(