Skip to content

Commit

Permalink
Udate minimum python version + annotations (#909)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelboulton authored Jan 20, 2024
1 parent 4e1276b commit 75db3c5
Show file tree
Hide file tree
Showing 48 changed files with 303 additions and 291 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
pull_request:
branches:
- master
- feature-2.0
- feat/3.0-release

jobs:
simple-checks:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ repos:
hooks:
- id: pyupgrade
args:
- --py38-plus
- --py311-plus
files: "tavern/.*"
- repo: https://github.com/rhysd/actionlint
rev: v1.6.26
Expand Down
2 changes: 1 addition & 1 deletion docs/source/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1520,7 +1520,7 @@ third block must start with 4 and the third block must start with 8, 9, "A", or
```

This is using the `!re_fullmatch` variant of the tag - this calls
[`re.fullmatch`](https://docs.python.org/3.8/library/re.html#re.fullmatch) under
[`re.fullmatch`](https://docs.python.org/3.11/library/re.html#re.fullmatch) under
the hood, which means that the regex given needs to match the _entire_ part of
the response that is being checked for it to pass. There is also `!re_search`
which will pass if it matches _part_ of the thing being checked, or `!re_match`
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Tavern is an advanced pytest based API testing framework for HTTP, MQTT or other protocols.

Note that Tavern **only** supports Python 3.4 and up. At the time of writing we
test against Python 3.8-3.10. Python 2 is now **unsupported**.
test against Python 3.11. Python 2 is now **unsupported**.

## Why Tavern

Expand Down
10 changes: 3 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ classifiers = [
"Intended Audience :: Developers",
"Framework :: Pytest",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Utilities",
"Topic :: Software Development :: Testing",
"License :: OSI Approved :: MIT License",
]
requires-python = ">=3.11"

keywords = ["testing", "pytest"]

Expand All @@ -37,8 +35,6 @@ dependencies = [
"stevedore>=4,<5",
]

requires-python = ">=3.10"

[[project.authors]]
name = "Michael Boulton"

Expand Down Expand Up @@ -122,7 +118,7 @@ paho-mqtt = "tavern._plugins.mqtt.tavernhook"
grpc = "tavern._plugins.grpc.tavernhook"

[tool.mypy]
python_version = 3.8
python_version = 3.11
ignore_missing_imports = true

[tool.coverage.run]
Expand Down Expand Up @@ -172,7 +168,7 @@ ignore = [
]
select = ["E", "F", "B", "W", "I", "S", "C4", "ICN", "T20", "PLE", "RUF", "SIM105", "PL"]
# Look at: UP
target-version = "py38"
target-version = "py311"
extend-exclude = [
"tests/unit/tavern_grpc/test_services_pb2.py",
"tests/unit/tavern_grpc/test_services_pb2.pyi",
Expand Down
3 changes: 1 addition & 2 deletions scripts/smoke.bash
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ set -ex
pre-commit run ruff --all-files || true
pre-commit run ruff-format --all-files || true

tox --parallel -c tox.ini \
-e py3check
tox --parallel -c tox.ini -e py3check || true

tox --parallel -c tox.ini \
-e py3mypy
Expand Down
64 changes: 37 additions & 27 deletions tavern/_core/dict_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import os
import re
import string
from typing import Any, Dict, List, Mapping, Union
from collections.abc import Collection, Iterable, Mapping, Sequence
from typing import Any, TypeVar

import box
import jmespath
Expand All @@ -22,10 +23,10 @@
from .formatted_str import FormattedString
from .strict_util import StrictSetting, StrictSettingKinds, extract_strict_setting

logger = logging.getLogger(__name__)
logger: logging.Logger = logging.getLogger(__name__)


def _check_and_format_values(to_format, box_vars: Mapping[str, Any]) -> str:
def _check_and_format_values(to_format: str, box_vars: Mapping[str, Any]) -> str:
formatter = string.Formatter()
would_format = formatter.parse(to_format)

Expand Down Expand Up @@ -93,16 +94,16 @@ def _attempt_find_include(to_format: str, box_vars: box.Box):


def format_keys(
val,
variables: Mapping,
val: TypeConvertToken | str | dict | list | tuple | Mapping | set,
variables: Mapping | Box,
*,
no_double_format: bool = True,
dangerously_ignore_string_format_errors: bool = False,
):
"""recursively format a dictionary with the given values
Args:
val: Input dictionary to format
val: Input to format
variables: Dictionary of keys to format it with
no_double_format: Whether to use the 'inner formatted string' class to avoid double formatting
This is required if passing something via pytest-xdist, such as markers:
Expand Down Expand Up @@ -130,7 +131,7 @@ def format_keys(
# formatted = {key: format_keys(val[key], box_vars) for key in val}
for key in val:
formatted[key] = format_keys_(val[key], box_vars)
elif isinstance(val, (list, tuple)):
elif isinstance(val, (list, tuple, set)):
formatted = [format_keys_(item, box_vars) for item in val] # type: ignore
elif isinstance(formatted, FormattedString):
logger.debug("Already formatted %s, not double-formatting", formatted)
Expand All @@ -156,7 +157,7 @@ def format_keys(
return formatted


def recurse_access_key(data, query: str):
def recurse_access_key(data: dict | list[str] | Mapping, query: str):
"""
Search for something in the given data using the given query.
Expand All @@ -168,8 +169,8 @@ def recurse_access_key(data, query: str):
'c'
Args:
data (dict, list): Data to search in
query (str): Query to run
data: Data to search in
query: Query to run
Returns:
object: Whatever was found by the search
Expand All @@ -181,7 +182,7 @@ def recurse_access_key(data, query: str):
logger.error("Error parsing JMES query")

try:
_deprecated_recurse_access_key(data, query.split("."))
_deprecated_recurse_access_key(data, query.split(".")) # type:ignore
except (IndexError, KeyError):
logger.debug("Nothing found searching using old method")
else:
Expand All @@ -195,7 +196,7 @@ def recurse_access_key(data, query: str):
return from_jmespath


def _deprecated_recurse_access_key(current_val, keys):
def _deprecated_recurse_access_key(current_val: dict, keys: list[str]):
"""Given a list of keys and a dictionary, recursively access the dicionary
using the keys until we find the key its looking for
Expand All @@ -209,8 +210,8 @@ def _deprecated_recurse_access_key(current_val, keys):
'c'
Args:
current_val (dict): current dictionary we have recursed into
keys (list): list of str/int of subkeys
current_val: current dictionary we have recursed into
keys: list of str/int of subkeys
Raises:
IndexError: list index not found in data
Expand All @@ -224,7 +225,7 @@ def _deprecated_recurse_access_key(current_val, keys):
if not keys:
return current_val
else:
current_key = keys.pop(0)
current_key: str | int = keys.pop(0)

with contextlib.suppress(ValueError):
current_key = int(current_key)
Expand All @@ -241,7 +242,7 @@ def _deprecated_recurse_access_key(current_val, keys):
raise


def deep_dict_merge(initial_dct: Dict, merge_dct: Mapping) -> dict:
def deep_dict_merge(initial_dct: dict, merge_dct: Mapping) -> dict:
"""Recursive dict merge. Instead of updating only top-level keys,
dict_merge recurses down into dicts nested to an arbitrary depth
and returns the merged dict. Keys values present in merge_dct take
Expand All @@ -266,12 +267,15 @@ def deep_dict_merge(initial_dct: Dict, merge_dct: Mapping) -> dict:
return dct


def check_expected_keys(expected, actual) -> None:
_CanCheck = Sequence | Mapping | set | Collection


def check_expected_keys(expected: _CanCheck, actual: _CanCheck) -> None:
"""Check that a set of expected keys is a superset of the actual keys
Args:
expected (list, set, dict): keys we expect
actual (list, set, dict): keys we have got from the input
expected: keys we expect
actual: keys we have got from the input
Raises:
UnexpectedKeysError: If not actual <= expected
Expand All @@ -289,7 +293,7 @@ def check_expected_keys(expected, actual) -> None:
raise exceptions.UnexpectedKeysError(msg)


def yield_keyvals(block):
def yield_keyvals(block: _CanCheck) -> Iterable[tuple[list[str], str, str]]:
"""Return indexes, keys and expected values for matching recursive keys
Given a list or dict, return a 3-tuple of the 'split' key (key split on
Expand Down Expand Up @@ -321,10 +325,10 @@ def yield_keyvals(block):
(['2'], '2', 'c')
Args:
block (dict, list): input matches
block: input matches
Yields:
(list, str, str): key split on dots, key, expected value
key split on dots, key, expected value
"""
if isinstance(block, dict):
for joined_key, expected_val in block.items():
Expand All @@ -336,10 +340,13 @@ def yield_keyvals(block):
yield [sidx], sidx, val


T = TypeVar("T", Mapping, set, Sequence, Collection)


def check_keys_match_recursive(
expected_val: Any,
actual_val: Any,
keys: List[Union[str, int]],
expected_val: T,
actual_val: T,
keys: list[str | int],
strict: StrictSettingKinds = True,
) -> None:
"""Utility to recursively check response values
Expand Down Expand Up @@ -443,7 +450,7 @@ def _format_err(which):
raise exceptions.KeyMismatchError(msg) from e

if isinstance(expected_val, dict):
akeys = set(actual_val.keys())
akeys = set(actual_val.keys()) # type:ignore
ekeys = set(expected_val.keys())

if akeys != ekeys:
Expand Down Expand Up @@ -481,7 +488,10 @@ def _format_err(which):
for key in to_recurse:
try:
check_keys_match_recursive(
expected_val[key], actual_val[key], keys + [key], strict
expected_val[key],
actual_val[key], # type:ignore
keys + [key],
strict,
)
except KeyError:
logger.debug(
Expand Down
4 changes: 2 additions & 2 deletions tavern/_core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Dict, Optional
from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
from tavern._core.pytest.config import TestConfig
Expand All @@ -15,7 +15,7 @@ class TavernException(Exception):
test_block_config: config for stage
"""

stage: Optional[Dict]
stage: dict | None
test_block_config: Optional["TestConfig"]
is_final: bool = False

Expand Down
7 changes: 4 additions & 3 deletions tavern/_core/extfunctions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import functools
import importlib
import logging
from typing import Any, List, Mapping, Optional
from collections.abc import Mapping
from typing import Any

from tavern._core import exceptions

Expand All @@ -21,7 +22,7 @@ def is_ext_function(block: Any) -> bool:
return isinstance(block, dict) and block.get("$ext", None) is not None


def get_pykwalify_logger(module: Optional[str]) -> logging.Logger:
def get_pykwalify_logger(module: str | None) -> logging.Logger:
"""Get logger for this module
Have to do it like this because the way that pykwalify load extension
Expand Down Expand Up @@ -140,7 +141,7 @@ def _get_ext_values(ext: Mapping):
return func, args, kwargs


def update_from_ext(request_args: dict, keys_to_check: List[str]) -> None:
def update_from_ext(request_args: dict, keys_to_check: list[str]) -> None:
"""
Updates the request_args dict with any values from external functions
Expand Down
5 changes: 2 additions & 3 deletions tavern/_core/general.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import logging
import os
from typing import List

from tavern._core.loader import load_single_document_yaml

from .dict_util import deep_dict_merge

logger = logging.getLogger(__name__)
logger: logging.Logger = logging.getLogger(__name__)


def load_global_config(global_cfg_paths: List[os.PathLike]) -> dict:
def load_global_config(global_cfg_paths: list[os.PathLike]) -> dict:
"""Given a list of file paths to global config files, load each of them and
return the joined dictionary.
Expand Down
5 changes: 3 additions & 2 deletions tavern/_core/jmesutils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import operator
import re
from typing import Any, Dict, List, Sized
from collections.abc import Sized
from typing import Any

from tavern._core import exceptions

Expand Down Expand Up @@ -37,7 +38,7 @@ def test_type(val, mytype) -> bool:
"regex": lambda x, y: regex_compare(str(x), str(y)),
"type": test_type,
}
TYPES: Dict[str, List[Any]] = {
TYPES: dict[str, list[Any]] = {
"none": [type(None)],
"number": [int, float],
"int": [int],
Expand Down
Loading

0 comments on commit 75db3c5

Please sign in to comment.