Skip to content

Commit

Permalink
Update f-strings (#910)
Browse files Browse the repository at this point in the history

Update to f-strings everywhere and use a couple of type annotations in plugins to make type checking easier
  • Loading branch information
michaelboulton committed Jan 20, 2024
1 parent 2f7728c commit 0322268
Show file tree
Hide file tree
Showing 36 changed files with 159 additions and 131 deletions.
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args:
- --py38-plus
files: "tavern/.*"
- repo: https://github.com/rhysd/actionlint
rev: v1.6.26
hooks:
Expand Down
10 changes: 6 additions & 4 deletions scripts/smoke.bash
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

set -ex

pre-commit run ruff --all-files
pre-commit run ruff-format --all-files
pre-commit run ruff --all-files || true
pre-commit run ruff-format --all-files || true

# Separate as isort can interfere with other testenvs
tox --parallel -c tox.ini \
-e py3check

tox --parallel -c tox.ini \
-e py3,py3mypy
-e py3mypy

tox --parallel -c tox.ini \
-e py3

tox -c tox-integration.ini \
-e py3-generic,py3-grpc,py3-mqtt
18 changes: 8 additions & 10 deletions tavern/_core/dict_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ def _attempt_find_include(to_format: str, box_vars: box.Box):

if field_name is None:
raise exceptions.InvalidFormattedJsonError(
"Invalid string used for {}".format(yaml_tag)
f"Invalid string used for {yaml_tag}"
)

pattern = r"{" + field_name + r".*}"

if not re.match(pattern, to_format):
raise exceptions.InvalidFormattedJsonError(
"Invalid format specifier '{}' for {}".format(to_format, yaml_tag)
f"Invalid format specifier '{to_format}' for {yaml_tag}"
)

if format_spec:
Expand Down Expand Up @@ -284,7 +284,7 @@ def check_expected_keys(expected, actual) -> None:

logger.debug("Valid keys = %s, actual keys = %s", expected, keyset)

msg = "Unexpected keys {}".format(unexpected)
msg = f"Unexpected keys {unexpected}"
logger.error(msg)
raise exceptions.UnexpectedKeysError(msg)

Expand Down Expand Up @@ -383,7 +383,7 @@ def full_err():
"""

def _format_err(which):
return "{}{}".format(which, "".join('["{}"]'.format(key) for key in keys))
return "{}{}".format(which, "".join(f'["{key}"]' for key in keys))

e_formatted = _format_err("expected")
a_formatted = _format_err("actual")
Expand Down Expand Up @@ -452,7 +452,7 @@ def _format_err(which):

msg = ""
if extra_actual_keys:
msg += " - Extra keys in response: {}".format(extra_actual_keys)
msg += f" - Extra keys in response: {extra_actual_keys}"
if extra_expected_keys:
msg += " - Keys missing from response: {}".format(
extra_expected_keys
Expand Down Expand Up @@ -531,7 +531,7 @@ def _format_err(which):
break

if missing:
msg = "List item(s) not present in response: {}".format(missing)
msg = f"List item(s) not present in response: {missing}"
raise exceptions.KeyMismatchError(msg) from e

logger.debug("All expected list items present")
Expand All @@ -557,7 +557,7 @@ def _format_err(which):
if isinstance(expected_val, RegexSentinel):
if not expected_val.passes(actual_val):
raise exceptions.KeyMismatchError(
"Regex mismatch: ({})".format(full_err())
f"Regex mismatch: ({full_err()})"
) from e

logger.debug(
Expand All @@ -566,9 +566,7 @@ def _format_err(which):
expected_val.constructor,
)
else:
raise exceptions.KeyMismatchError(
"Key mismatch: ({})".format(full_err())
) from e
raise exceptions.KeyMismatchError(f"Key mismatch: ({full_err()})") from e


def get_tavern_box() -> box.Box:
Expand Down
6 changes: 3 additions & 3 deletions tavern/_core/extfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,14 @@ def import_ext_function(entrypoint: str):
try:
imported = importlib.import_module(module)
except ImportError as e:
msg = "Error importing module {}".format(module)
msg = f"Error importing module {module}"
logger.exception(msg)
raise exceptions.InvalidExtFunctionError(msg) from e

try:
function = getattr(imported, funcname)
except AttributeError as e:
msg = "No function named {} in {}".format(funcname, module)
msg = f"No function named {funcname} in {module}"
logger.exception(msg)
raise exceptions.InvalidExtFunctionError(msg) from e

Expand Down Expand Up @@ -125,7 +125,7 @@ def inner():
def _get_ext_values(ext: Mapping):
if not isinstance(ext, Mapping):
raise exceptions.InvalidExtFunctionError(
"ext block should be a dict, but it was a {}".format(type(ext))
f"ext block should be a dict, but it was a {type(ext)}"
)

args = ext.get("extra_args") or ()
Expand Down
6 changes: 2 additions & 4 deletions tavern/_core/jmesutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def test_type(val, mytype) -> bool:
typelist = TYPES.get(str(mytype).lower())
if typelist is None:
raise TypeError(
"Type {0} is not a valid type to test against!".format(str(mytype).lower())
f"Type {str(mytype).lower()} is not a valid type to test against!"
)
try:
for testtype in typelist:
Expand Down Expand Up @@ -87,6 +87,4 @@ def actual_validation(
_operator: str, _actual, expected, _expression, expression
) -> None:
if not COMPARATORS[_operator](_actual, expected):
raise exceptions.JMESError(
"Validation '{}' ({}) failed!".format(expression, _expression)
)
raise exceptions.JMESError(f"Validation '{expression}' ({_expression}) failed!")
6 changes: 3 additions & 3 deletions tavern/_core/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def from_yaml(cls, loader, node):
return cls()

def __str__(self):
return "<Tavern YAML sentinel for {}>".format(self.constructor)
return f"<Tavern YAML sentinel for {self.constructor}>"

@classmethod
def to_yaml(cls, dumper, data):
Expand Down Expand Up @@ -243,7 +243,7 @@ class RegexSentinel(TypeSentinel):
compiled: re.Pattern

def __str__(self):
return "<Tavern Regex sentinel for {}>".format(self.compiled.pattern)
return f"<Tavern Regex sentinel for {self.compiled.pattern}>"

@property
def yaml_tag(self):
Expand Down Expand Up @@ -444,7 +444,7 @@ def load_single_document_yaml(filename: os.PathLike) -> dict:
UnexpectedDocumentsError: If more than one document was in the file
"""

with open(filename, "r", encoding="utf-8") as fileobj:
with open(filename, encoding="utf-8") as fileobj:
try:
contents = yaml.load(fileobj, Loader=IncludeLoader) # type:ignore # noqa
except yaml.composer.ComposerError as e:
Expand Down
72 changes: 52 additions & 20 deletions tavern/_core/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
import dataclasses
import logging
from functools import partial
from typing import Any, List, Mapping, Optional
from typing import Any, Callable, Dict, List, Mapping, Optional, Protocol, Type

import stevedore

from tavern._core import exceptions
from tavern._core.dict_util import format_keys
from tavern._core.pytest.config import TestConfig
from tavern.request import BaseRequest
from tavern.response import BaseResponse

logger = logging.getLogger(__name__)

Expand All @@ -24,11 +25,27 @@ class PluginHelperBase:

def plugin_load_error(mgr, entry_point, err):
"""Handle import errors"""
msg = "Error loading plugin {} - {}".format(entry_point, err)
msg = f"Error loading plugin {entry_point} - {err}"
raise exceptions.PluginLoadError(msg) from err


def is_valid_reqresp_plugin(ext: Any) -> bool:
class _TavernPlugin(Protocol):
"""A tavern plugin"""

session_type: Type[Any]
request_type: Type[BaseRequest]
verifier_type: Type[BaseResponse]
response_block_name: str
request_block_name: str
schema: Mapping

def get_expected_from_request(
self, response_block: BaseResponse, test_block_config: TestConfig, session: Any
) -> Any:
...


def is_valid_reqresp_plugin(ext: stevedore.extension.Extension) -> bool:
"""Whether this is a valid 'reqresp' plugin
Requires certain functions/variables to be present
Expand Down Expand Up @@ -59,25 +76,34 @@ def is_valid_reqresp_plugin(ext: Any) -> bool:
"schema",
]

return all(hasattr(ext.plugin, i) for i in required)
plugin: _TavernPlugin = ext.plugin

return all(hasattr(plugin, i) for i in required)


class _Plugin:
"""Wrapped tavern plugin for convenience"""

name: str
plugin: _TavernPlugin


@dataclasses.dataclass
class _PluginCache:
plugins: List[Any] = dataclasses.field(default_factory=list)
plugins: List[_Plugin] = dataclasses.field(default_factory=list)

def __call__(self, config: Optional[TestConfig] = None):
if not config and not self.plugins:
raise exceptions.PluginLoadError("No config to load plugins from")
elif self.plugins:
def __call__(self, config: Optional[TestConfig] = None) -> List[_Plugin]:
if self.plugins:
return self.plugins
elif not self.plugins and config:
# NOTE
# This is reloaded every time

if config:
# NOTE: This is reloaded every time
self.plugins = self._load_plugins(config)
return self.plugins

def _load_plugins(self, test_block_config: TestConfig) -> List[Any]:
raise exceptions.PluginLoadError("No config to load plugins from")

def _load_plugins(self, test_block_config: TestConfig) -> List[_Plugin]:
"""Load plugins from the 'tavern' entrypoint namespace
This can be a module or a class as long as it defines the right things
Expand Down Expand Up @@ -107,7 +133,7 @@ def enabled(current_backend, ext):
for backend in test_block_config.backends():
logger.debug("loading backend for %s", backend)

namespace = "tavern_{}".format(backend)
namespace = f"tavern_{backend}"

manager = stevedore.EnabledExtensionManager(
namespace=namespace,
Expand Down Expand Up @@ -194,11 +220,13 @@ def get_request_type(
keys[p.plugin.request_block_name] = p.plugin.request_type

if len(set(keys) & set(stage)) > 1:
logger.error("Can only specify 1 request type")
raise exceptions.DuplicateKeysError
raise exceptions.DuplicateKeysError(
f"Can only specify 1 request type but got {set(keys)}"
)
elif not list(set(keys) & set(stage)):
logger.error("Need to specify one of '%s'", keys.keys())
raise exceptions.MissingKeysError
raise exceptions.MissingKeysError(
f"Need to specify one of valid request types: '{set(keys.keys())}'"
)

# We've validated that 1 and only 1 is there, so just loop until the first
# one is found
Expand All @@ -224,13 +252,17 @@ class ResponseVerifier(dict):
plugin_name: str


def _foreach_response(stage: Mapping, test_block_config: TestConfig, action):
def _foreach_response(
stage: Mapping,
test_block_config: TestConfig,
action: Callable[[_Plugin, str], dict],
) -> Dict[str, dict]:
"""Do something for each response
Args:
stage: Stage of test
test_block_config: Config for test
action ((p: {plugin, name}, response_block: dict) -> Any): function that takes (plugin, response block)
action: function that takes (plugin, response block)
Returns:
mapping of plugin name to list of expected (normally length 1)
Expand Down
8 changes: 3 additions & 5 deletions tavern/_core/pytest/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def read_formatted_vars(lines):
white = True
red = False

line = " {} = '{}'".format(var[1:-1], value_at_call)
line = f" {var[1:-1]} = '{value_at_call}'"
tw.line(line, white=white, red=red) # pragma: no cover

return missing
Expand All @@ -127,9 +127,7 @@ def _print_test_stage(
line_start: Source line of this stage
"""
if line_start:
tw.line(
"Source test stage (line {}):".format(line_start), white=True, bold=True
)
tw.line(f"Source test stage (line {line_start}):", white=True, bold=True)
else:
tw.line("Source test stages:", white=True, bold=True)

Expand Down Expand Up @@ -167,7 +165,7 @@ def _print_formatted_stage(self, tw: TerminalWriter, stage: Mapping) -> None:
for line in formatted_lines:
if not line:
continue
tw.line(" {}".format(line), white=True)
tw.line(f" {line}", white=True)

def _print_errors(self, tw: TerminalWriter) -> None:
"""Print any errors in the 'normal' Pytest style
Expand Down
6 changes: 3 additions & 3 deletions tavern/_core/pytest/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _format_test_marks(original_marks, fmt_vars, test_name):
pytest_marks.append(getattr(pytest.mark, markname)(extra_arg))
formatted_marks.append({markname: extra_arg})
else:
raise exceptions.BadSchemaError("Unexpected mark type '{}'".format(type(m)))
raise exceptions.BadSchemaError(f"Unexpected mark type '{type(m)}'")

return pytest_marks, formatted_marks

Expand Down Expand Up @@ -221,14 +221,14 @@ def unwrap_map(value):

# Change the name
spec_new = copy.deepcopy(test_spec)
spec_new["test_name"] = test_spec["test_name"] + "[{}]".format(inner_formatted)
spec_new["test_name"] = test_spec["test_name"] + f"[{inner_formatted}]"

logger.debug("New test name: %s", spec_new["test_name"])

# Make this new thing available for formatting
spec_new.setdefault("includes", []).append(
{
"name": "parametrized[{}]".format(inner_formatted),
"name": f"parametrized[{inner_formatted}]",
"description": "autogenerated by Tavern",
"variables": variables,
}
Expand Down
4 changes: 2 additions & 2 deletions tavern/_core/pytest/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def obj(self):
name = stage["name"]
elif "id" in stage:
name = stage["id"]
stages.append("{:d}: {:s}".format(i + 1, name))
stages.append(f"{i + 1:d}: {name:s}")

# This needs to be a function or skipif breaks
def fakefun():
Expand Down Expand Up @@ -242,7 +242,7 @@ def runtest(self) -> None:
raise
else:
if xfail:
raise Exception("internal: xfail test did not fail '{}'".format(xfail))
raise Exception(f"internal: xfail test did not fail '{xfail}'")
finally:
call_hook(
self.global_cfg,
Expand Down

0 comments on commit 0322268

Please sign in to comment.