From 66b91edaa302ad08189ffb95f7d544a64d063499 Mon Sep 17 00:00:00 2001 From: rnetser Date: Wed, 26 Aug 2026 18:16:51 +0300 Subject: [PATCH 1/2] feat(resource)!: require DynamicClient client BREAKING CHANGE: client is mandatory on Resource/NamespacedResource/Event APIs. Remove dyn_client and config_file/config_dict/context from Resource APIs; build clients via get_client() instead. Co-authored-by: Cursor --- AGENTS.md | 4 +- examples/validation_demo.py | 16 +++- examples/validation_troubleshooting.py | 12 ++- ocp_resources/event.py | 40 +++----- ocp_resources/resource.py | 124 +++++++++---------------- tests/test_resource.py | 67 +++++++++++++ 6 files changed, 151 insertions(+), 112 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d3076ccfe2..a9ea959aa9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,14 +54,16 @@ Code between markers is auto-generated — do NOT modify manually. Use `class-ge ## When Writing Code +- **Always pass `client` when instantiating a resource** — `client: DynamicClient` is required on `Resource` / `NamespacedResource` (and on `.get()` / `Event` APIs). Create it with `get_client()` (or `get_client(fake=True)` in tests); do not rely on implicit kubeconfig/client creation. - No client-side validation in `to_dict()` — let the K8s/OCP API server return errors. Helper functions in resource classes CAN validate. -- Context managers for auto-cleanup: `with Pod(name="test", namespace="default") as pod:` +- Context managers for auto-cleanup: `with Pod(client=client, name="test", namespace="default") as pod:` - Wait utilities: `wait_for_status()`, `wait_for_condition()` via timeout-sampler - Sensitive data: add keys to `keys_to_hash` property for automatic log hashing - Fake client for tests: `get_client(fake=True)` — no cluster required ## When Reviewing Code +- Verify resources are constructed with an explicit `client=` (and `.get()` / `Event` calls pass `client`) - Verify generated code markers are intact — no manual edits between markers - Check type hints on all new functions/parameters - Verify tests exist for new helper methods (not needed for generated `__init__`/`to_dict`) diff --git a/examples/validation_demo.py b/examples/validation_demo.py index a03407f06a..b78cd87ea7 100644 --- a/examples/validation_demo.py +++ b/examples/validation_demo.py @@ -15,6 +15,8 @@ from ocp_resources.resource import get_client from ocp_resources.service import Service +FAKE_CLIENT = get_client(fake=True) + def print_section(title): """Print a section header""" @@ -30,6 +32,7 @@ def demo_basic_validation(): # Create a valid pod print("1. Creating a valid pod and validating...") pod = Pod( + client=FAKE_CLIENT, name="nginx-pod", namespace="default", containers=[{"name": "nginx", "image": "nginx:latest", "ports": [{"containerPort": 80}]}], @@ -44,6 +47,7 @@ def demo_basic_validation(): # Create an invalid pod (missing required fields) print("\n2. Creating an invalid pod (missing image)...") invalid_pod = Pod( + client=FAKE_CLIENT, name="invalid-pod", namespace="default", containers=[{"name": "nginx"}], # Missing required 'image' field @@ -61,7 +65,7 @@ def demo_auto_validation(): print_section(title="Auto-validation During Create") # Get a fake client for demo - client = get_client(fake=True) + client = FAKE_CLIENT # Create pod with auto-validation enabled print("1. Creating pod with auto-validation enabled...") @@ -141,6 +145,7 @@ def demo_different_resources(): # Service validation print("1. Validating a Service...") service = Service( + client=FAKE_CLIENT, name="nginx-service", namespace="default", selector={"app": "nginx"}, @@ -156,6 +161,7 @@ def demo_different_resources(): # ConfigMap validation print("\n2. Validating a ConfigMap...") config_map = ConfigMap( + client=FAKE_CLIENT, name="app-config", namespace="default", data={"app.properties": "debug=true\nport=8080", "database.url": "postgresql://localhost:5432/mydb"}, @@ -176,7 +182,12 @@ def demo_performance(): pods = [] for i in range(5): pods.append( - Pod(name=f"perf-test-pod-{i}", namespace="default", containers=[{"name": "nginx", "image": "nginx:latest"}]) + Pod( + client=FAKE_CLIENT, + name=f"perf-test-pod-{i}", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) ) # First validation (loads and caches schema) @@ -208,6 +219,7 @@ def demo_error_details(): # Create pod with multiple errors print("Creating a pod with multiple validation errors...") pod = Pod( + client=FAKE_CLIENT, name="invalid-name-", # Names must start and end with alphanumeric, can only contain lowercase alphanumeric or hyphens namespace="default", containers=[ diff --git a/examples/validation_troubleshooting.py b/examples/validation_troubleshooting.py index 39934e8d37..37e80e97e1 100644 --- a/examples/validation_troubleshooting.py +++ b/examples/validation_troubleshooting.py @@ -9,8 +9,11 @@ from ocp_resources.deployment import Deployment from ocp_resources.exceptions import ValidationError from ocp_resources.pod import Pod +from ocp_resources.resource import get_client from ocp_resources.service import Service +FAKE_CLIENT = get_client(fake=True) + def print_error_case(title, description): """Print an error case header""" @@ -35,6 +38,7 @@ def case_1_missing_required_fields(): print("Problem code:") print(""" pod = Pod( + client=client, name="my-pod", namespace="default", containers=[{"name": "nginx"}] # Missing 'image' @@ -42,7 +46,7 @@ def case_1_missing_required_fields(): """) try: - pod = Pod(name="my-pod", namespace="default", containers=[{"name": "nginx"}]) + pod = Pod(client=FAKE_CLIENT, name="my-pod", namespace="default", containers=[{"name": "nginx"}]) pod.validate() except ValidationError as e: print(f"Error: {e}") @@ -52,6 +56,7 @@ def case_1_missing_required_fields(): print("Fixed code:") print(""" pod = Pod( + client=client, name="my-pod", namespace="default", containers=[{ @@ -119,6 +124,7 @@ def case_3_invalid_field_values(): print("Problem code:") print(""" pod = Pod( + client=client, name="My-Pod-123", # Capital letters not allowed namespace="default" ) @@ -126,6 +132,7 @@ def case_3_invalid_field_values(): try: pod = Pod( + client=FAKE_CLIENT, name="My-Pod-123", # Invalid DNS name namespace="default", containers=[{"name": "nginx", "image": "nginx:latest"}], @@ -139,6 +146,7 @@ def case_3_invalid_field_values(): print("Fixed code:") print(""" pod = Pod( + client=client, name="my-pod-123", # Valid DNS name namespace="default" ) @@ -155,6 +163,7 @@ def case_4_invalid_structure(): print("Problem code:") print(""" service = Service( + client=client, name="my-service", namespace="default", selector={"app": "nginx"}, @@ -182,6 +191,7 @@ def case_4_invalid_structure(): print("Fixed code:") print(""" service = Service( + client=client, name="my-service", namespace="default", selector={"app": "nginx"}, diff --git a/ocp_resources/event.py b/ocp_resources/event.py index 0085a46e23..2414aba3d7 100644 --- a/ocp_resources/event.py +++ b/ocp_resources/event.py @@ -1,4 +1,3 @@ -import warnings from collections.abc import Generator from datetime import datetime, timedelta, timezone from typing import Any @@ -16,29 +15,10 @@ class Event: api_version = "v1" - # TODO: remove once `client` is mandatory - @staticmethod - def _resolve_client( - client: DynamicClient | None, - dyn_client: DynamicClient | None, - ) -> DynamicClient: - """Resolve client from new or deprecated parameter with deprecation warning.""" - if client is None and dyn_client is not None: - warnings.warn( - "`dyn_client` arg will be renamed to `client` and will be mandatory in the next major release.", - FutureWarning, - stacklevel=3, # Adjusted for helper function call - ) - - resolved = client or dyn_client - assert resolved is not None, "Either 'client' or 'dyn_client' must be provided" - return resolved - @classmethod def get( cls, - client: DynamicClient | None = None, # TODO: make mandatory in the next major release - dyn_client: DynamicClient | None = None, # TODO: remove in the next major release + client: DynamicClient, namespace: str | None = None, name: str | None = None, label_selector: str | None = None, @@ -51,7 +31,6 @@ def get( Args: client (DynamicClient): K8s client - dyn_client (DynamicClient): K8s client namespace (str): event namespace name (str): event name label_selector (str): filter events by labels; comma separated string of key=value @@ -72,7 +51,8 @@ def get( ): print(event.object) """ - _client = cls._resolve_client(client, dyn_client) + if client is None: + raise TypeError("client is required") LOGGER.info("Reading events") LOGGER.debug( @@ -81,7 +61,7 @@ def get( f" resource_version={resource_version}, timeout={timeout}" ) - event_listener = _client.resources.get(api_version=cls.api_version, kind=cls.__name__) + event_listener = client.resources.get(api_version=cls.api_version, kind=cls.__name__) yield from event_listener.watch( namespace=namespace, name=name, @@ -137,6 +117,9 @@ def list( field_selector="type==Warning", ) """ + if client is None: + raise TypeError("client is required") + if since_seconds < 0: raise ValueError("since_seconds must be >= 0") @@ -172,8 +155,7 @@ def list( @classmethod def delete_events( cls, - client: DynamicClient | None = None, # TODO: make mandatory in the next major release - dyn_client: DynamicClient | None = None, # TODO: remove in the next major release + client: DynamicClient, namespace: str | None = None, name: str | None = None, label_selector: str | None = None, @@ -187,7 +169,6 @@ def delete_events( Args: client (DynamicClient): K8s client - dyn_client (DynamicClient): K8s client namespace (str): event namespace name (str): event name label_selector (str): filter events by labels; comma separated string of key=value @@ -200,7 +181,8 @@ def delete_events( def delete_events_before_test(client): Event.delete_events(client=client, namespace="my-namespace", field_selector="reason=AnEventReason") """ - _client = cls._resolve_client(client, dyn_client) + if client is None: + raise TypeError("client is required") LOGGER.info("Deleting events") LOGGER.debug( @@ -209,7 +191,7 @@ def delete_events_before_test(client): f" resource_version={resource_version}, timeout={timeout}" ) - _client.resources.get(api_version=cls.api_version, kind=cls.__name__).delete( + client.resources.get(api_version=cls.api_version, kind=cls.__name__).delete( namespace=namespace, name=name, label_selector=label_selector, diff --git a/ocp_resources/resource.py b/ocp_resources/resource.py index 279b52cc94..dde405206c 100644 --- a/ocp_resources/resource.py +++ b/ocp_resources/resource.py @@ -6,7 +6,6 @@ import re import sys import threading -import warnings from abc import ABC, abstractmethod from collections.abc import Callable, Generator from io import StringIO @@ -193,6 +192,19 @@ def _exchange_code_for_token( ) +_REMOVED_CLIENT_KWARGS: frozenset[str] = frozenset({"dyn_client", "config_file", "config_dict", "context"}) + + +def _reject_removed_client_kwargs(kwargs: dict[str, Any]) -> None: + """Fail fast if deprecated client-config kwargs are passed via **kwargs.""" + removed = _REMOVED_CLIENT_KWARGS.intersection(kwargs) + if removed: + names = ", ".join(sorted(removed)) + raise TypeError( + f"Unsupported argument(s): {names}. Pass a DynamicClient via client= from get_client() instead." + ) + + def get_client( config_file: str | None = None, config_dict: dict[str, Any] | None = None, @@ -615,7 +627,7 @@ class ApiVersion: def __init__( self, - client: DynamicClient | None = None, # TODO: make mandatory in the next major release + client: DynamicClient, name: str | None = None, teardown: bool = True, yaml_file: str | None = None, @@ -623,9 +635,6 @@ def __init__( dry_run: bool = False, node_selector: dict[str, Any] | None = None, node_selector_labels: dict[str, str] | None = None, - config_file: str | None = None, - config_dict: dict[str, Any] | None = None, - context: str | None = None, label: dict[str, str] | None = None, annotations: dict[str, str] | None = None, api_group: str = "", @@ -641,16 +650,14 @@ def __init__( If `yaml_file` or `kind_dict` are passed, logic in `to_dict` is bypassed. Args: - name (str): Resource name client (DynamicClient): Dynamic client for connecting to a remote cluster + name (str): Resource name teardown (bool): Indicates if this resource would need to be deleted yaml_file (str): yaml file for the resource delete_timeout (int): timeout associated with delete action dry_run (bool): dry run node_selector (dict): node selector node_selector_labels (str): node selector labels - config_file (str): Path to config file for connecting to remote cluster. - context (str): Context name for connecting to remote cluster. label (dict): Resource labels annotations (dict[str, str] | None): Resource annotations api_group (str): Resource API group; will overwrite API group definition in resource class @@ -665,6 +672,9 @@ def __init__( if yaml_file and kind_dict: raise ValueError("yaml_file and resource_dict are mutually exclusive") + if client is None: + raise TypeError("client is required") + self.name = name self.teardown = teardown self.yaml_file = yaml_file @@ -673,19 +683,9 @@ def __init__( self.dry_run = dry_run self.node_selector = node_selector self.node_selector_labels = node_selector_labels - self.config_file = config_file - self.config_dict = config_dict or {} - self.context = context self.label = label self.annotations = annotations - if not client: - warnings.warn( - "'client' arg will be mandatory in the next major release. " - "`config_file` and `context` args will be removed.", - FutureWarning, - stacklevel=2, - ) - self.client: DynamicClient = client or get_client(config_file=self.config_file, context=self.context) + self.client = client self.api_group: str = api_group or self.api_group self.hash_log_data = hash_log_data @@ -1181,14 +1181,11 @@ def retry_cluster_exceptions( @classmethod def get( cls, - client: DynamicClient | None = None, # TODO: make mandatory in the next major release - dyn_client: DynamicClient | None = None, # TODO: remove in the next major release - config_file: str = "", + client: DynamicClient, + *, singular_name: str = "", exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS, raw: bool = False, - context: str | None = None, - *args: Any, **kwargs: Any, ) -> Generator[Any, None, None]: """ @@ -1196,9 +1193,6 @@ def get( Args: client (DynamicClient): k8s client - dyn_client (DynamicClient): Open connection to remote cluster. - config_file (str): Path to config file for connecting to remote cluster. - context (str): Context name for connecting to remote cluster. singular_name (str): Resource kind (in lowercase), in use where we have multiple matches for resource. raw (bool): If True return raw object. exceptions_dict (dict): Exceptions dict for TimeoutSampler @@ -1206,31 +1200,24 @@ def get( Returns: generator: Generator of Resources of cls.kind. """ - _client = client or dyn_client - - if not _client: - warnings.warn( - "`dyn_client` arg will be renamed to `client` and will be mandatory in the next major release. " - "`config_file` and `context` will be removed.", - FutureWarning, - stacklevel=2, - ) - _client = get_client(config_file=config_file, context=context) + if client is None: + raise TypeError("client is required") + _reject_removed_client_kwargs(kwargs) def _get() -> Generator["Resource|ResourceInstance", None, None]: - _resources = cls._prepare_resources(*args, client=_client, singular_name=singular_name, **kwargs) # type: ignore[misc] + _resources = cls._prepare_resources(client=client, singular_name=singular_name, **kwargs) try: for resource_field in _resources.items: if raw: yield _resources else: - yield cls(client=_client, name=resource_field.metadata.name) + yield cls(client=client, name=resource_field.metadata.name) except TypeError: if raw: yield _resources else: - yield cls(client=_client, name=_resources.metadata.name) + yield cls(client=client, name=_resources.metadata.name) return Resource.retry_cluster_exceptions(func=_get, exceptions_dict=exceptions_dict) @@ -1441,10 +1428,7 @@ def events( @staticmethod def get_all_cluster_resources( - client: DynamicClient | None = None, # TODO: make mandatory in the next major release - config_file: str = "", - context: str | None = None, - config_dict: dict[str, Any] | None = None, + client: DynamicClient, *args: Any, **kwargs: Any, ) -> Generator[ResourceField, None, None]: @@ -1453,31 +1437,26 @@ def get_all_cluster_resources( Args: client (DynamicClient): k8s client - config_file (str): path to a kubeconfig file. - config_dict (dict): dict with kubeconfig configuration. - context (str): name of the context to use. - *args (tuple): args to pass to client.get() **kwargs (dict): kwargs to pass to client.get() Yields: kubernetes.dynamic.resource.ResourceField: Cluster resource. Example: - for resource in get_all_cluster_resources(label_selector="my-label=value"): + for resource in get_all_cluster_resources(client=client, label_selector="my-label=value"): print(f"Resource: {resource}") """ - if not client: - warnings.warn( - "'client' arg will be mandatory in the next major release. " - "`config_file`, `config_dict` and `context` will be removed.", - FutureWarning, - stacklevel=2, + if client is None: + raise TypeError("client is required") + if args: + raise TypeError( + "get_all_cluster_resources() takes no positional arguments after client; use keyword arguments only" ) - client = get_client(config_file=config_file, config_dict=config_dict, context=context) + _reject_removed_client_kwargs(kwargs) for _resource in client.resources.search(): try: - _resources = client.get(_resource, *args, **kwargs) + _resources = client.get(_resource, **kwargs) yield from _resources.items except (NotFoundError, TypeError, MethodNotAllowedError): @@ -1625,12 +1604,12 @@ class NamespacedResource(Resource): def __init__( self, + client: DynamicClient, name: str | None = None, namespace: str | None = None, teardown: bool = True, yaml_file: str | None = None, delete_timeout: int = TIMEOUT_4MINUTES, - client: DynamicClient | None = None, ensure_exists: bool = False, **kwargs: Any, ): @@ -1652,14 +1631,11 @@ def __init__( @classmethod def get( cls, - client: DynamicClient | None = None, # TODO: make mandatory in the next major release - dyn_client: DynamicClient | None = None, # TODO: remove in the next major release - config_file: str = "", + client: DynamicClient, + *, singular_name: str = "", exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS, raw: bool = False, - context: str | None = None, - *args: Any, **kwargs: Any, ) -> Generator[Any, None, None]: """ @@ -1667,9 +1643,6 @@ def get( Args: client (DynamicClient): k8s client - dyn_client (DynamicClient): Open connection to remote cluster - config_file (str): Path to config file for connecting to remote cluster. - context (str): Context name for connecting to remote cluster. singular_name (str): Resource kind (in lowercase), in use where we have multiple matches for resource. raw (bool): If True return raw object. exceptions_dict (dict): Exceptions dict for TimeoutSampler @@ -1677,26 +1650,19 @@ def get( Returns: generator: Generator of Resources of cls.kind """ - _client = client or dyn_client - - if not _client: - warnings.warn( - "`dyn_client` arg will be renamed to `client` and will be mandatory in the next major release. " - "`config_file` and `context` will be removed.", - FutureWarning, - stacklevel=2, - ) - _client = get_client(config_file=config_file, context=context) + if client is None: + raise TypeError("client is required") + _reject_removed_client_kwargs(kwargs) def _get() -> Generator["NamespacedResource|ResourceInstance", None, None]: - _resources = cls._prepare_resources(*args, client=_client, singular_name=singular_name, **kwargs) # type: ignore[misc] + _resources = cls._prepare_resources(client=client, singular_name=singular_name, **kwargs) try: for resource_field in _resources.items: if raw: yield resource_field else: yield cls( - client=_client, + client=client, name=resource_field.metadata.name, namespace=resource_field.metadata.namespace, ) @@ -1705,7 +1671,7 @@ def _get() -> Generator["NamespacedResource|ResourceInstance", None, None]: yield _resources else: yield cls( - client=_client, + client=client, name=_resources.metadata.name, namespace=_resources.metadata.namespace, ) diff --git a/tests/test_resource.py b/tests/test_resource.py index bd409eca9e..6cdb7bfc5a 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -5,6 +5,7 @@ import pytest import yaml +from ocp_resources.event import Event from ocp_resources.exceptions import ResourceTeardownError from ocp_resources.namespace import Namespace from ocp_resources.pod import Pod @@ -96,6 +97,72 @@ def test_get_all_cluster_resources(self, fake_client): if _resources: break + def test_client_is_required(self, fake_client): + with pytest.raises(TypeError, match="client"): + Pod(name=BASE_POD_NAME, namespace="default", containers=POD_CONTAINERS) + + with pytest.raises(TypeError, match="client"): + Pod(client=None, name=BASE_POD_NAME, namespace="default", containers=POD_CONTAINERS) # type: ignore[arg-type] + + with pytest.raises(TypeError, match="client"): + list(Namespace.get()) + + with pytest.raises(TypeError, match="client"): + list(Namespace.get(client=None)) # type: ignore[arg-type] + + with pytest.raises(TypeError, match="client"): + list(Resource.get_all_cluster_resources()) + + with pytest.raises(TypeError, match="client"): + list(Resource.get_all_cluster_resources(client=None)) # type: ignore[arg-type] + + # Valid construction still works with an explicit client + pod = Pod(client=fake_client, name=BASE_POD_NAME, namespace="default", containers=POD_CONTAINERS) + assert pod.client is fake_client + + def test_removed_client_kwargs_rejected(self, fake_client): + with pytest.raises(TypeError, match="Unsupported argument"): + list(Namespace.get(client=fake_client, dyn_client=fake_client)) + + with pytest.raises(TypeError, match="Unsupported argument"): + list(Namespace.get(client=fake_client, config_file="/tmp/kubeconfig")) + + with pytest.raises(TypeError, match="Unsupported argument"): + list(Resource.get_all_cluster_resources(client=fake_client, context="default")) + + with pytest.raises(TypeError, match="Unsupported argument"): + list(Resource.get_all_cluster_resources(client=fake_client, config_dict={})) + + # Positional args after client are rejected (keyword-only API) + with pytest.raises(TypeError, match="positional"): + list(Namespace.get(fake_client, fake_client)) + + with pytest.raises(TypeError, match="positional"): + list(Resource.get_all_cluster_resources(fake_client, "extra")) + + # Resource.__init__ rejects removed kwargs as unexpected keyword arguments + with pytest.raises(TypeError, match="config_file"): + Pod( + client=fake_client, + name=BASE_POD_NAME, + namespace="default", + containers=POD_CONTAINERS, + config_file="/tmp/kubeconfig", + ) + + def test_event_client_is_required(self): + with pytest.raises(TypeError, match="client"): + list(Event.get()) + + with pytest.raises(TypeError, match="client"): + list(Event.get(client=None)) # type: ignore[arg-type] + + with pytest.raises(TypeError, match="client"): + Event.delete_events() + + with pytest.raises(TypeError, match="client"): + Event.delete_events(client=None) # type: ignore[arg-type] + def test_get_condition_message(self, pod): assert pod.get_condition_message( condition_type=pod.Condition.READY, condition_status=pod.Condition.Status.FALSE From a2f9000402e3ed5e7385e3e05facb9809d2a9559 Mon Sep 17 00:00:00 2001 From: rnetser Date: Wed, 26 Aug 2026 18:54:12 +0300 Subject: [PATCH 2/2] fix(resource): address Qodo type-hint and docstring findings Co-authored-by: Cursor --- ocp_resources/resource.py | 55 ++++++++++++++++++++++++--------------- tests/test_resource.py | 7 ++--- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/ocp_resources/resource.py b/ocp_resources/resource.py index dde405206c..0ff2596b41 100644 --- a/ocp_resources/resource.py +++ b/ocp_resources/resource.py @@ -196,7 +196,14 @@ def _exchange_code_for_token( def _reject_removed_client_kwargs(kwargs: dict[str, Any]) -> None: - """Fail fast if deprecated client-config kwargs are passed via **kwargs.""" + """Fail fast if deprecated client-config kwargs are passed via **kwargs. + + Args: + kwargs: Keyword arguments that may include removed client-config keys. + + Raises: + TypeError: If any of dyn_client, config_file, config_dict, or context are present. + """ removed = _REMOVED_CLIENT_KWARGS.intersection(kwargs) if removed: names = ", ".join(sorted(removed)) @@ -643,31 +650,37 @@ def __init__( kind_dict: dict[Any, Any] | None = None, wait_for_resource: bool = False, schema_validation_enabled: bool = False, - ): + ) -> None: """ - Create an API resource + Create an API resource. If `yaml_file` or `kind_dict` are passed, logic in `to_dict` is bypassed. Args: - client (DynamicClient): Dynamic client for connecting to a remote cluster - name (str): Resource name - teardown (bool): Indicates if this resource would need to be deleted - yaml_file (str): yaml file for the resource - delete_timeout (int): timeout associated with delete action - dry_run (bool): dry run - node_selector (dict): node selector - node_selector_labels (str): node selector labels - label (dict): Resource labels - annotations (dict[str, str] | None): Resource annotations - api_group (str): Resource API group; will overwrite API group definition in resource class - hash_log_data (bool): Hash resource content based on resource keys_to_hash property - (example: Secret resource) - ensure_exists (bool): Whether to check if the resource exists before when initializing the resource, raise if not. - kind_dict (dict): dict which represents the resource object - wait_for_resource (bool): Waits for the resource to be created - schema_validation_enabled (bool): Enable automatic schema validation for this instance. + client: Dynamic client for connecting to a remote cluster. + name: Resource name. + teardown: Indicates if this resource would need to be deleted. + yaml_file: Yaml file for the resource. + delete_timeout: Timeout associated with delete action. + dry_run: Dry run. + node_selector: Node selector. + node_selector_labels: Node selector labels. + label: Resource labels. + annotations: Resource annotations. + api_group: Resource API group; will overwrite API group definition in resource class. + hash_log_data: Hash resource content based on resource keys_to_hash property + (example: Secret resource). + ensure_exists: Whether to check if the resource exists before when initializing the resource, raise if not. + kind_dict: Dict which represents the resource object. + wait_for_resource: Waits for the resource to be created. + schema_validation_enabled: Enable automatic schema validation for this instance. Defaults to False. Set to True to validate on create/update operations. + + Raises: + ValueError: If both yaml_file and kind_dict are provided. + TypeError: If client is None. + NotImplementedError: If neither api_group nor api_version is defined on the class. + MissingRequiredArgumentError: If name is missing when yaml_file and kind_dict are not set. """ if yaml_file and kind_dict: raise ValueError("yaml_file and resource_dict are mutually exclusive") @@ -1612,7 +1625,7 @@ def __init__( delete_timeout: int = TIMEOUT_4MINUTES, ensure_exists: bool = False, **kwargs: Any, - ): + ) -> None: super().__init__( name=name, client=client, diff --git a/tests/test_resource.py b/tests/test_resource.py index 6cdb7bfc5a..54073c8a53 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -5,6 +5,7 @@ import pytest import yaml +from fake_kubernetes_client.dynamic_client import FakeDynamicClient from ocp_resources.event import Event from ocp_resources.exceptions import ResourceTeardownError from ocp_resources.namespace import Namespace @@ -97,7 +98,7 @@ def test_get_all_cluster_resources(self, fake_client): if _resources: break - def test_client_is_required(self, fake_client): + def test_client_is_required(self, fake_client: FakeDynamicClient) -> None: with pytest.raises(TypeError, match="client"): Pod(name=BASE_POD_NAME, namespace="default", containers=POD_CONTAINERS) @@ -120,7 +121,7 @@ def test_client_is_required(self, fake_client): pod = Pod(client=fake_client, name=BASE_POD_NAME, namespace="default", containers=POD_CONTAINERS) assert pod.client is fake_client - def test_removed_client_kwargs_rejected(self, fake_client): + def test_removed_client_kwargs_rejected(self, fake_client: FakeDynamicClient) -> None: with pytest.raises(TypeError, match="Unsupported argument"): list(Namespace.get(client=fake_client, dyn_client=fake_client)) @@ -150,7 +151,7 @@ def test_removed_client_kwargs_rejected(self, fake_client): config_file="/tmp/kubeconfig", ) - def test_event_client_is_required(self): + def test_event_client_is_required(self) -> None: with pytest.raises(TypeError, match="client"): list(Event.get())