From 995566470dfae352122641099d91174adf645a7b Mon Sep 17 00:00:00 2001 From: Andres Soto Date: Wed, 12 Nov 2025 13:38:22 -0500 Subject: [PATCH] fix: add PEP 561 py.typed markers for type checker support Add py.typed marker files to core and all module packages to indicate type information is available. This enables type checkers like Pyright and mypy to recognize and validate type hints in testcontainers packages. Resolves "Stub file not found" errors when running type checkers on code that imports testcontainers modules. --- core/testcontainers/compose/__init__.py | 1 - modules/generic/example_basic.py | 2 +- modules/postgres/example_basic.py | 4 ++-- .../testcontainers/postgres/__init__.py | 13 ++++++----- modules/postgres/tests/test_postgres.py | 22 ++++++++++++++----- pyproject.toml | 2 +- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/core/testcontainers/compose/__init__.py b/core/testcontainers/compose/__init__.py index 8eb8e100d..dd1aff658 100644 --- a/core/testcontainers/compose/__init__.py +++ b/core/testcontainers/compose/__init__.py @@ -1,4 +1,3 @@ -# flake8: noqa: F401 from testcontainers.compose.compose import ( ComposeContainer, DockerCompose, diff --git a/modules/generic/example_basic.py b/modules/generic/example_basic.py index 107bcc7c2..f5a7a43aa 100644 --- a/modules/generic/example_basic.py +++ b/modules/generic/example_basic.py @@ -59,7 +59,7 @@ def basic_example(): print(f"\nPython container ID: {container_id}") # Execute command in container - exit_code, output = python.exec_run("python -c 'print(\"Hello from container!\")'") + _exit_code, output = python.exec(["python", "-c", 'print("Hello from container!")']) print(f"Command output: {output.decode()}") # Example 5: Container with health check diff --git a/modules/postgres/example_basic.py b/modules/postgres/example_basic.py index 611081023..0005bbd4f 100644 --- a/modules/postgres/example_basic.py +++ b/modules/postgres/example_basic.py @@ -1,11 +1,11 @@ -import pandas as pd +import pandas as pd # type: ignore[import-untyped] import sqlalchemy from sqlalchemy import text from testcontainers.postgres import PostgresContainer -def basic_example(): +def basic_example() -> None: with PostgresContainer() as postgres: # Get connection URL connection_url = postgres.get_connection_url() diff --git a/modules/postgres/testcontainers/postgres/__init__.py b/modules/postgres/testcontainers/postgres/__init__.py index bde21d5b3..23dfa1d69 100644 --- a/modules/postgres/testcontainers/postgres/__init__.py +++ b/modules/postgres/testcontainers/postgres/__init__.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. import os -from typing import Optional +from typing import Any, Optional, Union, cast from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter @@ -53,13 +53,14 @@ def __init__( password: Optional[str] = None, dbname: Optional[str] = None, driver: Optional[str] = "psycopg2", - **kwargs, + **kwargs: Any, ) -> None: raise_for_deprecated_parameter(kwargs, "user", "username") super().__init__(image=image, **kwargs) - self.username: str = username or os.environ.get("POSTGRES_USER", "test") - self.password: str = password or os.environ.get("POSTGRES_PASSWORD", "test") - self.dbname: str = dbname or os.environ.get("POSTGRES_DB", "test") + # Ensure concrete str types while preserving "falsy" fallback semantics + self.username: str = cast("str", username or os.environ.get("POSTGRES_USER", "test")) + self.password: str = cast("str", password or os.environ.get("POSTGRES_PASSWORD", "test")) + self.dbname: str = cast("str", dbname or os.environ.get("POSTGRES_DB", "test")) self.port = port self.driver = f"+{driver}" if driver else "" @@ -70,7 +71,7 @@ def _configure(self) -> None: self.with_env("POSTGRES_PASSWORD", self.password) self.with_env("POSTGRES_DB", self.dbname) - def get_connection_url(self, host: Optional[str] = None, driver: Optional[str] = _UNSET) -> str: + def get_connection_url(self, host: Optional[str] = None, driver: Union[str, None, object] = _UNSET) -> str: """Get a DB connection URL to connect to the PG DB. If a driver is set in the constructor (defaults to psycopg2!), the URL will contain the diff --git a/modules/postgres/tests/test_postgres.py b/modules/postgres/tests/test_postgres.py index 93b99d25f..a6dff162a 100644 --- a/modules/postgres/tests/test_postgres.py +++ b/modules/postgres/tests/test_postgres.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import TypedDict import pytest import sqlalchemy @@ -70,7 +71,13 @@ def test_quoted_password(): password = "p@$%25+0&%rd :/!=?" quoted_password = "p%40%24%2525+0%26%25rd %3A%2F%21%3D%3F" driver = "psycopg2" - kwargs = { + + class ConnKwargs(TypedDict, total=False): + driver: str + username: str + password: str + + kwargs: ConnKwargs = { "driver": driver, "username": user, "password": password, @@ -106,15 +113,20 @@ def test_show_how_to_initialize_db_via_initdb_dir(): with engine.begin() as connection: connection.execute(sqlalchemy.text(insert_query)) result = connection.execute(sqlalchemy.text(select_query)) - result = result.fetchall() - assert len(result) == 1 - assert result[0] == (1, "sally", "sells seashells") + rows = result.fetchall() + assert len(rows) == 1 + assert rows[0] == (1, "sally", "sells seashells") def test_none_driver_urls(): user = "root" password = "pass" - kwargs = { + + class ConnKwargs(TypedDict, total=False): + username: str + password: str + + kwargs: ConnKwargs = { "username": user, "password": password, } diff --git a/pyproject.toml b/pyproject.toml index 1a0231c51..fc3770bb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -354,7 +354,7 @@ mypy_path = [ # "modules/openfga", # "modules/opensearch", # "modules/oracle", - # "modules/postgres", + "modules/postgres", # "modules/rabbitmq", # "modules/redis", # "modules/selenium"