diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index e98c30cb..33f18878 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index 52ae53e3..212ed230 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "pytest-testinfra" description = "Test infrastructures" -requires-python = ">=3.9" +requires-python = ">=3.10" dynamic = ["version"] readme = "README.rst" license-files = ["LICENSE"] @@ -21,9 +21,11 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Testing", "Topic :: System :: Systems Administration", "Framework :: Pytest", diff --git a/ruff.toml b/ruff.toml index d50530a3..ac1c4cdb 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py39" +target-version = "py310" [lint] select = [ diff --git a/testinfra/backend/ansible.py b/testinfra/backend/ansible.py index 36e3c62c..34178243 100644 --- a/testinfra/backend/ansible.py +++ b/testinfra/backend/ansible.py @@ -13,7 +13,7 @@ import json import logging import pprint -from typing import Any, Optional +from typing import Any from testinfra.backend import base from testinfra.utils.ansible_runner import AnsibleRunner @@ -28,9 +28,9 @@ class AnsibleBackend(base.BaseBackend): def __init__( self, host: str, - ansible_inventory: Optional[str] = None, - ssh_config: Optional[str] = None, - ssh_identity_file: Optional[str] = None, + ansible_inventory: str | None = None, + ssh_config: str | None = None, + ssh_identity_file: str | None = None, force_ansible: bool = False, *args: Any, **kwargs: Any, @@ -73,7 +73,7 @@ def run(self, command: str, *args: str, **kwargs: Any) -> base.CommandResult: return self.result(rc, self.encode(command), stdout, stderr) def run_ansible( - self, module_name: str, module_args: Optional[str] = None, **kwargs: Any + self, module_name: str, module_args: str | None = None, **kwargs: Any ) -> Any: def get_encoding() -> str: return self.encoding diff --git a/testinfra/backend/base.py b/testinfra/backend/base.py index afde01a8..a7844964 100644 --- a/testinfra/backend/base.py +++ b/testinfra/backend/base.py @@ -17,7 +17,7 @@ import shlex import subprocess import urllib.parse -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: import testinfra.host @@ -28,9 +28,9 @@ @dataclasses.dataclass class HostSpec: name: str - port: Optional[str] - user: Optional[str] - password: Optional[str] + port: str | None + user: str | None + password: str | None @dataclasses.dataclass @@ -55,8 +55,8 @@ class CommandResult: backend: "BaseBackend" exit_status: int command: bytes - _stdout: Union[str, bytes] - _stderr: Union[str, bytes] + _stdout: str | bytes + _stderr: str | bytes @property def succeeded(self) -> bool: @@ -141,12 +141,12 @@ def __init__( self, hostname: str, sudo: bool = False, - sudo_user: Optional[str] = None, + sudo_user: str | None = None, *args: Any, **kwargs: Any, ): - self._encoding: Optional[str] = None - self._host: Optional[testinfra.host.Host] = None + self._encoding: str | None = None + self._host: testinfra.host.Host | None = None self.hostname = hostname self.sudo = sudo self.sudo_user = sudo_user @@ -207,7 +207,7 @@ def quote(command: str, *args: str) -> str: return command % tuple(shlex.quote(a) for a in args) return command - def get_sudo_command(self, command: str, sudo_user: Optional[str]) -> str: + def get_sudo_command(self, command: str, sudo_user: str | None) -> str: if sudo_user is None: return self.quote("sudo /bin/sh -c %s", command) return self.quote("sudo -u %s /bin/sh -c %s", sudo_user, command) @@ -265,7 +265,7 @@ def parse_hostspec(hostspec: str) -> HostSpec: return HostSpec(name, port, user, password) @staticmethod - def parse_containerspec(containerspec: str) -> tuple[str, Optional[str]]: + def parse_containerspec(containerspec: str) -> tuple[str, str | None]: name = containerspec user = None if "@" in name: @@ -311,7 +311,7 @@ def encode(self, data: str) -> bytes: return data.encode(self.encoding) def result( - self, rc: int, cmd: bytes, stdout: Union[str, bytes], stderr: Union[str, bytes] + self, rc: int, cmd: bytes, stdout: str | bytes, stderr: str | bytes ) -> CommandResult: result = CommandResult( backend=self, diff --git a/testinfra/backend/paramiko.py b/testinfra/backend/paramiko.py index 7530269b..d1fc5aa4 100644 --- a/testinfra/backend/paramiko.py +++ b/testinfra/backend/paramiko.py @@ -21,7 +21,7 @@ ) from None import functools -from typing import Any, Optional +from typing import Any import paramiko.pkey import paramiko.ssh_exception @@ -44,8 +44,8 @@ class ParamikoBackend(base.BaseBackend): def __init__( self, hostspec: str, - ssh_config: Optional[str] = None, - ssh_identity_file: Optional[str] = None, + ssh_config: str | None = None, + ssh_identity_file: str | None = None, timeout: int = 10, *args: Any, **kwargs: Any, diff --git a/testinfra/backend/salt.py b/testinfra/backend/salt.py index 5e779fc9..45be64ff 100644 --- a/testinfra/backend/salt.py +++ b/testinfra/backend/salt.py @@ -17,7 +17,7 @@ "You must install salt package to use the salt backend" ) from None -from typing import Any, Optional +from typing import Any from testinfra.backend import base @@ -28,7 +28,7 @@ class SaltBackend(base.BaseBackend): def __init__(self, host: str, *args: Any, **kwargs: Any): self.host = host - self._client: Optional[salt.client.LocalClient] = None + self._client: salt.client.LocalClient | None = None super().__init__(self.host, *args, **kwargs) @property diff --git a/testinfra/backend/ssh.py b/testinfra/backend/ssh.py index 3eaf5432..a4392e32 100644 --- a/testinfra/backend/ssh.py +++ b/testinfra/backend/ssh.py @@ -11,7 +11,7 @@ # limitations under the License. import base64 -from typing import Any, Optional +from typing import Any from testinfra.backend import base @@ -24,12 +24,12 @@ class SshBackend(base.BaseBackend): def __init__( self, hostspec: str, - ssh_config: Optional[str] = None, - ssh_identity_file: Optional[str] = None, + ssh_config: str | None = None, + ssh_identity_file: str | None = None, timeout: int = 10, - controlpath: Optional[str] = None, + controlpath: str | None = None, controlpersist: int = 60, - ssh_extra_args: Optional[str] = None, + ssh_extra_args: str | None = None, *args: Any, **kwargs: Any, ): diff --git a/testinfra/backend/winrm.py b/testinfra/backend/winrm.py index 563b50b2..3814ffbb 100644 --- a/testinfra/backend/winrm.py +++ b/testinfra/backend/winrm.py @@ -11,7 +11,7 @@ # limitations under the License. import re -from typing import Any, Optional +from typing import Any from testinfra.backend import base @@ -52,8 +52,8 @@ def __init__( hostspec: str, no_ssl: bool = False, no_verify_ssl: bool = False, - read_timeout_sec: Optional[int] = None, - operation_timeout_sec: Optional[int] = None, + read_timeout_sec: int | None = None, + operation_timeout_sec: int | None = None, *args: Any, **kwargs: Any, ): diff --git a/testinfra/modules/socket.py b/testinfra/modules/socket.py index 6abde563..11320191 100644 --- a/testinfra/modules/socket.py +++ b/testinfra/modules/socket.py @@ -12,7 +12,6 @@ import functools import socket -from typing import Optional from testinfra.modules.base import Module @@ -121,7 +120,7 @@ def is_listening(self): ) @property - def clients(self) -> list[Optional[tuple[str, int]]]: + def clients(self) -> list[tuple[str, int] | None]: """Return a list of clients connected to a listening socket For tcp and udp sockets a list of pair (address, port) is returned. @@ -134,7 +133,7 @@ def clients(self) -> list[Optional[tuple[str, int]]]: [None, None, None] """ - sockets: list[Optional[tuple[str, int]]] = [] + sockets: list[tuple[str, int] | None] = [] for sock in self._iter_sockets(False): if sock[0] != self.protocol: continue diff --git a/testinfra/utils/ansible_runner.py b/testinfra/utils/ansible_runner.py index eebaa553..a70dec03 100644 --- a/testinfra/utils/ansible_runner.py +++ b/testinfra/utils/ansible_runner.py @@ -17,8 +17,8 @@ import json import os import tempfile -from collections.abc import Iterator -from typing import Any, Callable, Optional, Union +from collections.abc import Callable, Iterator +from typing import Any import testinfra import testinfra.host @@ -50,7 +50,7 @@ def get_ansible_config() -> configparser.ConfigParser: def get_ansible_inventory( - config: configparser.ConfigParser, inventory_file: Optional[str] + config: configparser.ConfigParser, inventory_file: str | None ) -> Inventory: # Disable ansible verbosity to avoid # https://github.com/ansible/ansible/issues/59973 @@ -66,9 +66,9 @@ def get_ansible_host( config: configparser.ConfigParser, inventory: Inventory, host: str, - ssh_config: Optional[str] = None, - ssh_identity_file: Optional[str] = None, -) -> Optional[testinfra.host.Host]: + ssh_config: str | None = None, + ssh_identity_file: str | None = None, +) -> testinfra.host.Host | None: if is_empty_inventory(inventory): if host == "localhost": return testinfra.get_host("local://") @@ -139,9 +139,7 @@ def get_ansible_host( }, } - def get_config( - name: str, default: Union[None, bool, str] = None - ) -> Union[None, bool, str]: + def get_config(name: str, default: None | bool | str = None) -> None | bool | str: value = default option = options.get(name, {}) @@ -164,7 +162,7 @@ def get_config( password = get_config("ansible_ssh_pass") port = get_config("ansible_port") - kwargs: dict[str, Union[None, str, bool]] = {} + kwargs: dict[str, None | str | bool] = {} if get_config("ansible_become", False): kwargs["sudo"] = True kwargs["sudo_user"] = get_config("ansible_become_user") @@ -233,7 +231,7 @@ def is_empty_inventory(inventory: Inventory) -> bool: class AnsibleRunner: - _runners: dict[Optional[str], "AnsibleRunner"] = {} + _runners: dict[str | None, "AnsibleRunner"] = {} _known_options = { # Boolean arguments. "become": { @@ -272,9 +270,9 @@ class AnsibleRunner: }, } - def __init__(self, inventory_file: Optional[str] = None): + def __init__(self, inventory_file: str | None = None): self.inventory_file = inventory_file - self._host_cache: dict[str, Optional[testinfra.host.Host]] = {} + self._host_cache: dict[str, testinfra.host.Host | None] = {} super().__init__() def get_hosts(self, pattern: str = "all") -> list[str]: @@ -327,7 +325,7 @@ def get_variables(self, host: str) -> dict[str, Any]: hostvars.setdefault("groups", groups) return hostvars - def get_host(self, host: str, **kwargs: Any) -> Optional[testinfra.host.Host]: + def get_host(self, host: str, **kwargs: Any) -> testinfra.host.Host | None: try: return self._host_cache[host] except KeyError: @@ -369,8 +367,8 @@ def run_module( self, host: str, module_name: str, - module_args: Optional[str], - get_encoding: Optional[Callable[[], str]] = None, + module_args: str | None, + get_encoding: Callable[[], str] | None = None, **options: Any, ) -> Any: cmd, args = "ansible --tree %s", [] @@ -411,7 +409,7 @@ def run_module( return json.load(f) @classmethod - def get_runner(cls, inventory: Optional[str]) -> "AnsibleRunner": + def get_runner(cls, inventory: str | None) -> "AnsibleRunner": try: return cls._runners[inventory] except KeyError: