Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"

[lint]
select = [
Expand Down
10 changes: 5 additions & 5 deletions testinfra/backend/ansible.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions testinfra/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions testinfra/backend/paramiko.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
) from None

import functools
from typing import Any, Optional
from typing import Any

import paramiko.pkey
import paramiko.ssh_exception
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions testinfra/backend/salt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions testinfra/backend/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# limitations under the License.

import base64
from typing import Any, Optional
from typing import Any

from testinfra.backend import base

Expand All @@ -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,
):
Expand Down
6 changes: 3 additions & 3 deletions testinfra/backend/winrm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# limitations under the License.

import re
from typing import Any, Optional
from typing import Any

from testinfra.backend import base

Expand Down Expand Up @@ -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,
):
Expand Down
5 changes: 2 additions & 3 deletions testinfra/modules/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import functools
import socket
from typing import Optional

from testinfra.modules.base import Module

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
32 changes: 15 additions & 17 deletions testinfra/utils/ansible_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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://")
Expand Down Expand Up @@ -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, {})

Expand All @@ -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")
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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", []
Expand Down Expand Up @@ -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:
Expand Down