diff --git a/pyproject.toml b/pyproject.toml index 3d4d12787..d13255b57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,15 @@ types-PyYAML = ">=6.0.12" pytest-codspeed = ">=2.2.0" [tool.pytest.ini_options] -addopts = '--durations=10 --ignore=singer_sdk/helpers/_simpleeval.py -m "not external"' +addopts = [ + "--durations=10", + "--ignore=singer_sdk/helpers/_simpleeval.py", + "-m", + "not external", + "-ra", + "--strict-config", + "--strict-markers", +] filterwarnings = [ "error", "ignore:Could not configure external gitlab tests:UserWarning", @@ -167,13 +175,16 @@ filterwarnings = [ # https://github.com/joblib/joblib/pull/1518 "ignore:Attribute n is deprecated:DeprecationWarning:joblib._utils", ] +log_cli_level = "INFO" markers = [ "external: Tests relying on external resources", "windows: Tests that only run on Windows", "snapshot: Tests that use pytest-snapshot", ] +minversion = "7" testpaths = ["tests"] norecursedirs = "cookiecutter" +xfail_strict = false [tool.commitizen] name = "cz_version_bump" @@ -257,11 +268,14 @@ DEP002 = [ ] [tool.mypy] +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] exclude = "tests" files = "singer_sdk" local_partial_types = true +strict = false warn_redundant_casts = true warn_return_any = true +warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true @@ -295,7 +309,6 @@ extend-exclude = [ "*simpleeval*", ] line-length = 88 -src = ["samples", "singer_sdk", "tests"] target-version = "py38" [tool.ruff.format] diff --git a/singer_sdk/_singerlib/encoding/_simple.py b/singer_sdk/_singerlib/encoding/_simple.py index 7ce148fc3..5c8fec549 100644 --- a/singer_sdk/_singerlib/encoding/_simple.py +++ b/singer_sdk/_singerlib/encoding/_simple.py @@ -161,9 +161,9 @@ def __post_init__(self) -> None: self.type = SingerMessageType.SCHEMA if isinstance(self.bookmark_properties, (str, bytes)): - self.bookmark_properties = [self.bookmark_properties] + self.bookmark_properties = [self.bookmark_properties] # type: ignore[unreachable] if self.bookmark_properties and not isinstance(self.bookmark_properties, list): - msg = "bookmark_properties must be a string or list of strings" + msg = "bookmark_properties must be a string or list of strings" # type: ignore[unreachable] raise ValueError(msg) diff --git a/singer_sdk/authenticators.py b/singer_sdk/authenticators.py index c6478cb92..4c15304e5 100644 --- a/singer_sdk/authenticators.py +++ b/singer_sdk/authenticators.py @@ -71,7 +71,7 @@ def __call__(cls, *args: t.Any, **kwargs: t.Any) -> t.Any: # noqa: ANN401 A singleton instance of the derived class. """ if cls.__single_instance: - return cls.__single_instance + return cls.__single_instance # type: ignore[unreachable] single_obj = cls.__new__(cls, None) # type: ignore[call-overload] single_obj.__init__(*args, **kwargs) cls.__single_instance = single_obj @@ -165,7 +165,7 @@ def __init__( """ super().__init__(stream=stream) if self.auth_headers is None: - self.auth_headers = {} + self.auth_headers = {} # type: ignore[unreachable] if auth_headers: self.auth_headers.update(auth_headers) @@ -206,11 +206,11 @@ def __init__( if location == "header": if self.auth_headers is None: - self.auth_headers = {} + self.auth_headers = {} # type: ignore[unreachable] self.auth_headers.update(auth_credentials) elif location == "params": if self.auth_params is None: - self.auth_params = {} + self.auth_params = {} # type: ignore[unreachable] self.auth_params.update(auth_credentials) @classmethod @@ -255,7 +255,7 @@ def __init__(self, stream: RESTStream, token: str) -> None: auth_credentials = {"Authorization": f"Bearer {token}"} if self.auth_headers is None: - self.auth_headers = {} + self.auth_headers = {} # type: ignore[unreachable] self.auth_headers.update(auth_credentials) @classmethod @@ -314,7 +314,7 @@ def __init__( auth_credentials = {"Authorization": f"Basic {auth_token}"} if self.auth_headers is None: - self.auth_headers = {} + self.auth_headers = {} # type: ignore[unreachable] self.auth_headers.update(auth_credentials) @classmethod diff --git a/singer_sdk/connectors/sql.py b/singer_sdk/connectors/sql.py index b6a74a976..c9f18cbf3 100644 --- a/singer_sdk/connectors/sql.py +++ b/singer_sdk/connectors/sql.py @@ -496,7 +496,7 @@ def discover_catalog_entry( # Detect key properties possible_primary_keys: list[list[str]] = [] pk_def = inspected.get_pk_constraint(table_name, schema=schema_name) - if pk_def and "constrained_columns" in pk_def: + if pk_def and "constrained_columns" in pk_def: # type: ignore[redundant-expr] possible_primary_keys.append(pk_def["constrained_columns"]) # An element of the columns list is ``None`` if it's an expression and is @@ -953,7 +953,7 @@ def merge_sql_types( # Get the generic type class for opt in sql_types: # Get the length - opt_len: int = getattr(opt, "length", 0) + opt_len: int | None = getattr(opt, "length", 0) generic_type = type(opt.as_generic()) if isinstance(generic_type, type): diff --git a/singer_sdk/helpers/_batch.py b/singer_sdk/helpers/_batch.py index b57002e1d..2e4ae4615 100644 --- a/singer_sdk/helpers/_batch.py +++ b/singer_sdk/helpers/_batch.py @@ -232,7 +232,7 @@ def __post_init__(self) -> None: self.storage = StorageTarget.from_dict(self.storage) if self.batch_size is None: - self.batch_size = DEFAULT_BATCH_SIZE + self.batch_size = DEFAULT_BATCH_SIZE # type: ignore[unreachable] def asdict(self) -> dict[str, t.Any]: """Return a dictionary representation of the message. diff --git a/singer_sdk/helpers/_catalog.py b/singer_sdk/helpers/_catalog.py index 90eec4c5e..a8747d30c 100644 --- a/singer_sdk/helpers/_catalog.py +++ b/singer_sdk/helpers/_catalog.py @@ -43,7 +43,7 @@ def _pop_deselected_schema( schema_at_breadcrumb = schema_at_breadcrumb.get(crumb, {}) if not isinstance(schema_at_breadcrumb, dict): # pragma: no cover - msg = ( + msg = ( # type: ignore[unreachable] "Expected dictionary type instead of " f"'{type(schema_at_breadcrumb).__name__}' '{schema_at_breadcrumb}' for " f"'{stream_name}' bookmark '{breadcrumb!s}' in '{schema}'" @@ -123,7 +123,7 @@ def set_catalog_stream_selected( """ breadcrumb = breadcrumb or () if not isinstance(breadcrumb, tuple): # pragma: no cover - msg = ( + msg = ( # type: ignore[unreachable] f"Expected tuple value for breadcrumb '{breadcrumb}'. Got " f"{type(breadcrumb).__name__}" ) diff --git a/singer_sdk/helpers/_state.py b/singer_sdk/helpers/_state.py index a910bb71e..fd7dee377 100644 --- a/singer_sdk/helpers/_state.py +++ b/singer_sdk/helpers/_state.py @@ -118,7 +118,7 @@ def get_writeable_state_dict( ValueError: Raise an error if duplicate entries are found. """ if tap_state is None: - msg = "Cannot write state to missing state dictionary." + msg = "Cannot write state to missing state dictionary." # type: ignore[unreachable] raise ValueError(msg) if "bookmarks" not in tap_state: diff --git a/singer_sdk/mapper.py b/singer_sdk/mapper.py index a2e7bc956..0214fe9e9 100644 --- a/singer_sdk/mapper.py +++ b/singer_sdk/mapper.py @@ -377,7 +377,7 @@ def _eval_type( ValueError: If the expression is ``None``. """ if expr is None: - msg = "Expression should be str, not None" + msg = "Expression should be str, not None" # type: ignore[unreachable] raise ValueError(msg) default = default or th.StringType() @@ -564,7 +564,7 @@ def always_true(record: dict) -> bool: elif filter_rule is None: filter_fn = always_true else: - msg = ( + msg = ( # type: ignore[unreachable] f"Unexpected filter rule type '{type(filter_rule).__name__}' in " f"expression {filter_rule!s}. Expected 'str' or 'None'." ) @@ -783,9 +783,7 @@ def register_raw_stream_schema( # noqa: PLR0912, C901 key_properties=key_properties, flattening_options=self.flattening_options, ) - elif stream_def is None or ( - isinstance(stream_def, str) and stream_def == NULL_STRING - ): + elif stream_def is None or (stream_def == NULL_STRING): mapper = RemoveRecordTransform( stream_alias=stream_alias, raw_schema=schema, @@ -800,7 +798,7 @@ def register_raw_stream_schema( # noqa: PLR0912, C901 raise StreamMapConfigError(msg) else: - msg = ( + msg = ( # type: ignore[unreachable] f"Unexpected stream definition type. Expected str, dict, or None. " f"Got '{type(stream_def).__name__}'." ) diff --git a/singer_sdk/plugin_base.py b/singer_sdk/plugin_base.py index eb5b39de3..ed37bfb3f 100644 --- a/singer_sdk/plugin_base.py +++ b/singer_sdk/plugin_base.py @@ -153,7 +153,7 @@ def __init__( elif isinstance(config, dict): config_dict = config else: - msg = f"Error parsing config of type '{type(config).__name__}'." + msg = f"Error parsing config of type '{type(config).__name__}'." # type: ignore[unreachable] raise ValueError(msg) if parse_env_config: self.logger.info("Parsing env var for settings config...") diff --git a/singer_sdk/streams/core.py b/singer_sdk/streams/core.py index 51a968313..e65f577f2 100644 --- a/singer_sdk/streams/core.py +++ b/singer_sdk/streams/core.py @@ -162,7 +162,7 @@ def __init__( elif isinstance(schema, singer.Schema): self._schema = schema.to_dict() else: - msg = f"Unexpected type {type(schema).__name__} for arg 'schema'." + msg = f"Unexpected type {type(schema).__name__} for arg 'schema'." # type: ignore[unreachable] raise ValueError(msg) if self.schema_filepath: @@ -187,7 +187,7 @@ def stream_maps(self) -> list[StreamMap]: if self._stream_maps: return self._stream_maps - if self._tap.mapper: + if self._tap.mapper: # type: ignore[truthy-bool] self._stream_maps = self._tap.mapper.stream_maps[self.name] self.logger.info( "Tap has custom mapper. Using %d provided map(s).", diff --git a/singer_sdk/streams/graphql.py b/singer_sdk/streams/graphql.py index 4e5455bc3..22a5fb0cc 100644 --- a/singer_sdk/streams/graphql.py +++ b/singer_sdk/streams/graphql.py @@ -71,7 +71,7 @@ def prepare_request_payload( query = self.query if query is None: - msg = "Graphql `query` property not set." + msg = "Graphql `query` property not set." # type: ignore[unreachable] raise ValueError(msg) if not query.lstrip().startswith("query"): diff --git a/singer_sdk/tap_base.py b/singer_sdk/tap_base.py index d69fa5f38..d246a2790 100644 --- a/singer_sdk/tap_base.py +++ b/singer_sdk/tap_base.py @@ -148,7 +148,7 @@ def state(self) -> dict: RuntimeError: If state has not been initialized. """ if self._state is None: - msg = "Could not read from uninitialized state." + msg = "Could not read from uninitialized state." # type: ignore[unreachable] raise RuntimeError(msg) return self._state @@ -398,7 +398,7 @@ def load_state(self, state: dict[str, t.Any]) -> None: initialized. """ if self.state is None: - msg = "Cannot write to uninitialized state dictionary." + msg = "Cannot write to uninitialized state dictionary." # type: ignore[unreachable] raise ValueError(msg) for stream_name, stream_state in state.get("bookmarks", {}).items(): diff --git a/singer_sdk/testing/legacy.py b/singer_sdk/testing/legacy.py index a47d3e770..eae4c5b5d 100644 --- a/singer_sdk/testing/legacy.py +++ b/singer_sdk/testing/legacy.py @@ -40,7 +40,7 @@ def _test_discovery() -> None: catalog1 = _get_tap_catalog(tap_class, config or {}) # Reset and re-initialize with an input catalog tap2: Tap = tap_class(config=config, parse_env_config=True, catalog=catalog1) - assert tap2 + assert tap2 # type: ignore[truthy-bool] def _test_stream_connections() -> None: # Initialize with basic config diff --git a/singer_sdk/testing/tap_tests.py b/singer_sdk/testing/tap_tests.py index 5839e0cea..92e0b13e1 100644 --- a/singer_sdk/testing/tap_tests.py +++ b/singer_sdk/testing/tap_tests.py @@ -48,7 +48,7 @@ def test(self) -> None: catalog=catalog, **kwargs, ) - assert tap2 + assert tap2 # type: ignore[truthy-bool] class TapStreamConnectionTest(TapTestTemplate): @@ -218,7 +218,7 @@ def test(self) -> None: try: for v in self.non_null_attribute_values: error_message = f"Unable to parse value ('{v}') with datetime parser." - assert datetime_fromisoformat(v), error_message + assert datetime_fromisoformat(v), error_message # type: ignore[truthy-bool] except ValueError as e: raise AssertionError(error_message) from e diff --git a/singer_sdk/typing.py b/singer_sdk/typing.py index 6bf8d9527..2910125f8 100644 --- a/singer_sdk/typing.py +++ b/singer_sdk/typing.py @@ -189,7 +189,7 @@ def __get__(self, instance: P, owner: type[P]) -> t.Any: # noqa: ANN401 The property value. """ if instance is None: - instance = owner() + instance = owner() # type: ignore[unreachable] return self.fget(instance) @@ -1123,13 +1123,10 @@ def to_jsonschema_type( type_name = from_type elif isinstance(from_type, sa.types.TypeEngine): type_name = type(from_type).__name__ - elif isinstance(from_type, type) and issubclass( - from_type, - sa.types.TypeEngine, - ): + elif issubclass(from_type, sa.types.TypeEngine): type_name = from_type.__name__ else: # pragma: no cover - msg = "Expected `str` or a SQLAlchemy `TypeEngine` object or type." + msg = "Expected `str` or a SQLAlchemy `TypeEngine` object or type." # type: ignore[unreachable] # TODO: this should be a TypeError, but it's a breaking change. raise ValueError(msg) # noqa: TRY004