Split up yaml loaders into multiple files (#23774)

* Start moving parts of yaml utils to own module

Move parts of yaml loader out of the single large file and start
to create the structure of the yaml loaders in Ansible [0].

[0]: https://github.com/ansible/ansible/tree/devel/lib/ansible/parsing/yaml

* Finish yaml migration, update tests and mocks

  * Move code around to finish the migration
  * Update the mocks so that `open` is patched in
    `homeassistant.util.yaml.loader` instead of
    `homeassistant.util.yaml`.
  * Updated mypy ignores
  * Updated external API of `homeasistant.util.yaml`, see below:

Checked what part of the api of `homeassistant.util.yaml` was actually
called from outside the tests and added an `__ALL__` that contains only
these elements.

Updated the tests so that references to internal parts of the API (e.g.
the yaml module imported into `homeassistant.util.yaml.loader`) are
referenced directly from `homeassistant.util.yaml.loader`.

In `tests/test_yaml.py` the import `yaml` refers to
`homeassistant.util.yaml` and `yaml_loader` refers to `~.loader`.

Future work that remains for the next iteration is to create a custom
SafeConstructor and refers to that instead of monkey patching `yaml` with
custom loaders.

* Update mocks in yaml dumper, check_config
pull/23784/head
Ties de Kock 2019-05-09 09:07:56 -07:00 committed by Paulus Schoutsen
parent 118d3bc11c
commit 4004867eda
11 changed files with 179 additions and 139 deletions

View File

@ -17,7 +17,8 @@ from homeassistant.config import (
CONF_PACKAGES, merge_packages_config, _format_config_error,
find_config_file, load_yaml_config_file,
extract_domain_configs, config_per_platform)
from homeassistant.util import yaml
import homeassistant.util.yaml.loader as yaml_loader
from homeassistant.exceptions import HomeAssistantError
REQUIREMENTS = ('colorlog==4.0.2',)
@ -25,12 +26,14 @@ REQUIREMENTS = ('colorlog==4.0.2',)
_LOGGER = logging.getLogger(__name__)
# pylint: disable=protected-access
MOCKS = {
'load': ("homeassistant.util.yaml.load_yaml", yaml.load_yaml),
'load*': ("homeassistant.config.load_yaml", yaml.load_yaml),
'secrets': ("homeassistant.util.yaml.secret_yaml", yaml.secret_yaml),
'load': ("homeassistant.util.yaml.loader.load_yaml",
yaml_loader.load_yaml),
'load*': ("homeassistant.config.load_yaml", yaml_loader.load_yaml),
'secrets': ("homeassistant.util.yaml.loader.secret_yaml",
yaml_loader.secret_yaml),
}
SILENCE = (
'homeassistant.scripts.check_config.yaml.clear_secret_cache',
'homeassistant.scripts.check_config.yaml_loader.clear_secret_cache',
)
PATCHES = {}
@ -195,7 +198,8 @@ def check(config_dir, secrets=False):
if secrets:
# Ensure !secrets point to the patched function
yaml.yaml.SafeLoader.add_constructor('!secret', yaml.secret_yaml)
yaml_loader.yaml.SafeLoader.add_constructor('!secret',
yaml_loader.secret_yaml)
try:
hass = core.HomeAssistant()
@ -203,7 +207,7 @@ def check(config_dir, secrets=False):
res['components'] = hass.loop.run_until_complete(
check_ha_config_file(hass))
res['secret_cache'] = OrderedDict(yaml.__SECRET_CACHE)
res['secret_cache'] = OrderedDict(yaml_loader.__SECRET_CACHE)
for err in res['components'].errors:
domain = err.domain or ERROR_STR
@ -221,7 +225,8 @@ def check(config_dir, secrets=False):
pat.stop()
if secrets:
# Ensure !secrets point to the original function
yaml.yaml.SafeLoader.add_constructor('!secret', yaml.secret_yaml)
yaml_loader.yaml.SafeLoader.add_constructor(
'!secret', yaml_loader.secret_yaml)
bootstrap.clear_secret_cache()
return res
@ -239,7 +244,7 @@ def line_info(obj, **kwargs):
def dump_dict(layer, indent_count=3, listi=False, **kwargs):
"""Display a dict.
A friendly version of print yaml.yaml.dump(config).
A friendly version of print yaml_loader.yaml.dump(config).
"""
def sort_dict_key(val):
"""Return the dict key for sorting."""
@ -311,7 +316,7 @@ async def check_ha_config_file(hass):
return result.add_error(
"Error loading {}: {}".format(config_path, err))
finally:
yaml.clear_secret_cache()
yaml_loader.clear_secret_cache()
# Extract and validate core [homeassistant] config
try:

View File

@ -0,0 +1,15 @@
"""YAML utility functions."""
from .const import (
SECRET_YAML, _SECRET_NAMESPACE
)
from .dumper import dump, save_yaml
from .loader import (
clear_secret_cache, load_yaml, secret_yaml
)
__all__ = [
'SECRET_YAML', '_SECRET_NAMESPACE',
'dump', 'save_yaml',
'clear_secret_cache', 'load_yaml', 'secret_yaml',
]

View File

@ -0,0 +1,4 @@
"""Constants."""
SECRET_YAML = 'secrets.yaml'
_SECRET_NAMESPACE = 'homeassistant'

View File

@ -0,0 +1,60 @@
"""Custom dumper and representers."""
from collections import OrderedDict
import yaml
from .objects import NodeListClass
def dump(_dict: dict) -> str:
"""Dump YAML to a string and remove null."""
return yaml.safe_dump(
_dict, default_flow_style=False, allow_unicode=True) \
.replace(': null\n', ':\n')
def save_yaml(path: str, data: dict) -> None:
"""Save YAML to a file."""
# Dump before writing to not truncate the file if dumping fails
str_data = dump(data)
with open(path, 'w', encoding='utf-8') as outfile:
outfile.write(str_data)
# From: https://gist.github.com/miracle2k/3184458
# pylint: disable=redefined-outer-name
def represent_odict(dump, tag, mapping, # type: ignore
flow_style=None) -> yaml.MappingNode:
"""Like BaseRepresenter.represent_mapping but does not issue the sort()."""
value = [] # type: list
node = yaml.MappingNode(tag, value, flow_style=flow_style)
if dump.alias_key is not None:
dump.represented_objects[dump.alias_key] = node
best_style = True
if hasattr(mapping, 'items'):
mapping = mapping.items()
for item_key, item_value in mapping:
node_key = dump.represent_data(item_key)
node_value = dump.represent_data(item_value)
if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style):
best_style = False
if not (isinstance(node_value, yaml.ScalarNode) and
not node_value.style):
best_style = False
value.append((node_key, node_value))
if flow_style is None:
if dump.default_flow_style is not None:
node.flow_style = dump.default_flow_style
else:
node.flow_style = best_style
return node
yaml.SafeDumper.add_representer(
OrderedDict,
lambda dumper, value:
represent_odict(dumper, 'tag:yaml.org,2002:map', value))
yaml.SafeDumper.add_representer(
NodeListClass,
lambda dumper, value:
dumper.represent_sequence('tag:yaml.org,2002:seq', value))

View File

@ -1,4 +1,4 @@
"""YAML utility functions."""
"""Custom loader."""
import logging
import os
import sys
@ -7,6 +7,7 @@ from collections import OrderedDict
from typing import Union, List, Dict, Iterator, overload, TypeVar
import yaml
try:
import keyring
except ImportError:
@ -19,25 +20,23 @@ except ImportError:
from homeassistant.exceptions import HomeAssistantError
from .const import _SECRET_NAMESPACE, SECRET_YAML
from .objects import NodeListClass, NodeStrClass
_LOGGER = logging.getLogger(__name__)
_SECRET_NAMESPACE = 'homeassistant'
SECRET_YAML = 'secrets.yaml'
__SECRET_CACHE = {} # type: Dict[str, JSON_TYPE]
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
DICT_T = TypeVar('DICT_T', bound=Dict) # pylint: disable=invalid-name
class NodeListClass(list):
"""Wrapper class to be able to add attributes on a list."""
def clear_secret_cache() -> None:
"""Clear the secret cache.
pass
class NodeStrClass(str):
"""Wrapper class to be able to add attributes on a string."""
pass
Async friendly.
"""
__SECRET_CACHE.clear()
# pylint: disable=too-many-ancestors
@ -54,6 +53,21 @@ class SafeLineLoader(yaml.SafeLoader):
return node
def load_yaml(fname: str) -> JSON_TYPE:
"""Load a YAML file."""
try:
with open(fname, encoding='utf-8') as conf_file:
# If configuration file is empty YAML returns None
# We convert that to an empty dict
return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict()
except yaml.YAMLError as exc:
_LOGGER.error(str(exc))
raise HomeAssistantError(exc)
except UnicodeDecodeError as exc:
_LOGGER.error("Unable to read file %s: %s", fname, exc)
raise HomeAssistantError(exc)
# pylint: disable=pointless-statement
@overload
def _add_reference(obj: Union[list, NodeListClass],
@ -86,44 +100,6 @@ def _add_reference(obj, loader: SafeLineLoader, # type: ignore # noqa: F811
return obj
def load_yaml(fname: str) -> JSON_TYPE:
"""Load a YAML file."""
try:
with open(fname, encoding='utf-8') as conf_file:
# If configuration file is empty YAML returns None
# We convert that to an empty dict
return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict()
except yaml.YAMLError as exc:
_LOGGER.error(str(exc))
raise HomeAssistantError(exc)
except UnicodeDecodeError as exc:
_LOGGER.error("Unable to read file %s: %s", fname, exc)
raise HomeAssistantError(exc)
def dump(_dict: dict) -> str:
"""Dump YAML to a string and remove null."""
return yaml.safe_dump(
_dict, default_flow_style=False, allow_unicode=True) \
.replace(': null\n', ':\n')
def save_yaml(path: str, data: dict) -> None:
"""Save YAML to a file."""
# Dump before writing to not truncate the file if dumping fails
str_data = dump(data)
with open(path, 'w', encoding='utf-8') as outfile:
outfile.write(str_data)
def clear_secret_cache() -> None:
"""Clear the secret cache.
Async friendly.
"""
__SECRET_CACHE.clear()
def _include_yaml(loader: SafeLineLoader,
node: yaml.nodes.Node) -> JSON_TYPE:
"""Load another YAML file and embeds it using the !include tag.
@ -331,43 +307,3 @@ yaml.SafeLoader.add_constructor('!include_dir_merge_list',
yaml.SafeLoader.add_constructor('!include_dir_named', _include_dir_named_yaml)
yaml.SafeLoader.add_constructor('!include_dir_merge_named',
_include_dir_merge_named_yaml)
# From: https://gist.github.com/miracle2k/3184458
# pylint: disable=redefined-outer-name
def represent_odict(dump, tag, mapping, # type: ignore
flow_style=None) -> yaml.MappingNode:
"""Like BaseRepresenter.represent_mapping but does not issue the sort()."""
value = [] # type: list
node = yaml.MappingNode(tag, value, flow_style=flow_style)
if dump.alias_key is not None:
dump.represented_objects[dump.alias_key] = node
best_style = True
if hasattr(mapping, 'items'):
mapping = mapping.items()
for item_key, item_value in mapping:
node_key = dump.represent_data(item_key)
node_value = dump.represent_data(item_value)
if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style):
best_style = False
if not (isinstance(node_value, yaml.ScalarNode) and
not node_value.style):
best_style = False
value.append((node_key, node_value))
if flow_style is None:
if dump.default_flow_style is not None:
node.flow_style = dump.default_flow_style
else:
node.flow_style = best_style
return node
yaml.SafeDumper.add_representer(
OrderedDict,
lambda dumper, value:
represent_odict(dumper, 'tag:yaml.org,2002:map', value))
yaml.SafeDumper.add_representer(
NodeListClass,
lambda dumper, value:
dumper.represent_sequence('tag:yaml.org,2002:seq', value))

View File

@ -0,0 +1,13 @@
"""Custom yaml object types."""
class NodeListClass(list):
"""Wrapper class to be able to add attributes on a list."""
pass
class NodeStrClass(str):
"""Wrapper class to be able to add attributes on a string."""
pass

View File

@ -17,7 +17,11 @@ disallow_untyped_defs = true
[mypy-homeassistant.config_entries]
disallow_untyped_defs = false
[mypy-homeassistant.util.yaml]
[mypy-homeassistant.util.yaml.dumper]
warn_return_any = false
disallow_untyped_calls = false
[mypy-homeassistant.util.yaml.loader]
warn_return_any = false
disallow_untyped_calls = false

View File

@ -15,7 +15,8 @@ from io import StringIO
from unittest.mock import MagicMock, Mock, patch
import homeassistant.util.dt as date_util
import homeassistant.util.yaml as yaml
import homeassistant.util.yaml.loader as yaml_loader
import homeassistant.util.yaml.dumper as yaml_dumper
from homeassistant import auth, config_entries, core as ha, loader
from homeassistant.auth import (
@ -680,7 +681,8 @@ def patch_yaml_files(files_dict, endswith=True):
# Not found
raise FileNotFoundError("File not found: {}".format(fname))
return patch.object(yaml, 'open', mock_open_f, create=True)
return patch.object(yaml_loader, 'open', mock_open_f, create=True)
return patch.object(yaml_dumper, 'open', mock_open_f, create=True)
def mock_coro(return_value=None, exception=None):

View File

@ -4,7 +4,7 @@ import unittest
from homeassistant.setup import setup_component
from homeassistant.components import light, scene
from homeassistant.util import yaml
from homeassistant.util.yaml import loader as yaml_loader
from tests.common import get_test_home_assistant
from tests.components.light import common as common_light
@ -90,7 +90,7 @@ class TestScene(unittest.TestCase):
self.light_1.entity_id, self.light_2.entity_id)
with io.StringIO(config) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.load(file)
assert setup_component(self.hass, scene.DOMAIN, doc)
common.activate(self.hass, 'scene.test')

View File

@ -11,7 +11,7 @@ from homeassistant.helpers import entity_registry
from tests.common import mock_registry, flush_store
YAML__OPEN_PATH = 'homeassistant.util.yaml.open'
YAML__OPEN_PATH = 'homeassistant.util.yaml.loader.open'
@pytest.fixture

View File

@ -8,7 +8,8 @@ from unittest.mock import patch
import pytest
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import yaml
from homeassistant.util.yaml import loader as yaml_loader
import homeassistant.util.yaml as yaml
from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file
from tests.common import get_test_config_dir, patch_yaml_files
@ -16,7 +17,7 @@ from tests.common import get_test_config_dir, patch_yaml_files
@pytest.fixture(autouse=True)
def mock_credstash():
"""Mock credstash so it doesn't connect to the internet."""
with patch.object(yaml, 'credstash') as mock_credstash:
with patch.object(yaml_loader, 'credstash') as mock_credstash:
mock_credstash.getSecret.return_value = None
yield mock_credstash
@ -25,7 +26,7 @@ def test_simple_list():
"""Test simple list."""
conf = "config:\n - simple\n - list"
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc['config'] == ["simple", "list"]
@ -33,7 +34,7 @@ def test_simple_dict():
"""Test simple dict."""
conf = "key: value"
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc['key'] == 'value'
@ -58,7 +59,7 @@ def test_environment_variable():
os.environ["PASSWORD"] = "secret_password"
conf = "password: !env_var PASSWORD"
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc['password'] == "secret_password"
del os.environ["PASSWORD"]
@ -67,7 +68,7 @@ def test_environment_variable_default():
"""Test config file with default value for environment variable."""
conf = "password: !env_var PASSWORD secret_password"
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc['password'] == "secret_password"
@ -76,7 +77,7 @@ def test_invalid_environment_variable():
conf = "password: !env_var PASSWORD"
with pytest.raises(HomeAssistantError):
with io.StringIO(conf) as file:
yaml.yaml.safe_load(file)
yaml_loader.yaml.safe_load(file)
def test_include_yaml():
@ -84,17 +85,17 @@ def test_include_yaml():
with patch_yaml_files({'test.yaml': 'value'}):
conf = 'key: !include test.yaml'
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc["key"] == "value"
with patch_yaml_files({'test.yaml': None}):
conf = 'key: !include test.yaml'
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc["key"] == {}
@patch('homeassistant.util.yaml.os.walk')
@patch('homeassistant.util.yaml.loader.os.walk')
def test_include_dir_list(mock_walk):
"""Test include dir list yaml."""
mock_walk.return_value = [
@ -107,11 +108,11 @@ def test_include_dir_list(mock_walk):
}):
conf = "key: !include_dir_list /tmp"
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc["key"] == sorted(["one", "two"])
@patch('homeassistant.util.yaml.os.walk')
@patch('homeassistant.util.yaml.loader.os.walk')
def test_include_dir_list_recursive(mock_walk):
"""Test include dir recursive list yaml."""
mock_walk.return_value = [
@ -129,13 +130,13 @@ def test_include_dir_list_recursive(mock_walk):
with io.StringIO(conf) as file:
assert '.ignore' in mock_walk.return_value[0][1], \
"Expecting .ignore in here"
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert 'tmp2' in mock_walk.return_value[0][1]
assert '.ignore' not in mock_walk.return_value[0][1]
assert sorted(doc["key"]) == sorted(["zero", "one", "two"])
@patch('homeassistant.util.yaml.os.walk')
@patch('homeassistant.util.yaml.loader.os.walk')
def test_include_dir_named(mock_walk):
"""Test include dir named yaml."""
mock_walk.return_value = [
@ -149,11 +150,11 @@ def test_include_dir_named(mock_walk):
conf = "key: !include_dir_named /tmp"
correct = {'first': 'one', 'second': 'two'}
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc["key"] == correct
@patch('homeassistant.util.yaml.os.walk')
@patch('homeassistant.util.yaml.loader.os.walk')
def test_include_dir_named_recursive(mock_walk):
"""Test include dir named yaml."""
mock_walk.return_value = [
@ -172,13 +173,13 @@ def test_include_dir_named_recursive(mock_walk):
with io.StringIO(conf) as file:
assert '.ignore' in mock_walk.return_value[0][1], \
"Expecting .ignore in here"
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert 'tmp2' in mock_walk.return_value[0][1]
assert '.ignore' not in mock_walk.return_value[0][1]
assert doc["key"] == correct
@patch('homeassistant.util.yaml.os.walk')
@patch('homeassistant.util.yaml.loader.os.walk')
def test_include_dir_merge_list(mock_walk):
"""Test include dir merge list yaml."""
mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]]
@ -189,11 +190,11 @@ def test_include_dir_merge_list(mock_walk):
}):
conf = "key: !include_dir_merge_list /tmp"
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert sorted(doc["key"]) == sorted(["one", "two", "three"])
@patch('homeassistant.util.yaml.os.walk')
@patch('homeassistant.util.yaml.loader.os.walk')
def test_include_dir_merge_list_recursive(mock_walk):
"""Test include dir merge list yaml."""
mock_walk.return_value = [
@ -211,14 +212,14 @@ def test_include_dir_merge_list_recursive(mock_walk):
with io.StringIO(conf) as file:
assert '.ignore' in mock_walk.return_value[0][1], \
"Expecting .ignore in here"
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert 'tmp2' in mock_walk.return_value[0][1]
assert '.ignore' not in mock_walk.return_value[0][1]
assert sorted(doc["key"]) == sorted(["one", "two",
"three", "four"])
@patch('homeassistant.util.yaml.os.walk')
@patch('homeassistant.util.yaml.loader.os.walk')
def test_include_dir_merge_named(mock_walk):
"""Test include dir merge named yaml."""
mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]]
@ -231,7 +232,7 @@ def test_include_dir_merge_named(mock_walk):
with patch_yaml_files(files):
conf = "key: !include_dir_merge_named /tmp"
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert doc["key"] == {
"key1": "one",
"key2": "two",
@ -239,7 +240,7 @@ def test_include_dir_merge_named(mock_walk):
}
@patch('homeassistant.util.yaml.os.walk')
@patch('homeassistant.util.yaml.loader.os.walk')
def test_include_dir_merge_named_recursive(mock_walk):
"""Test include dir merge named yaml."""
mock_walk.return_value = [
@ -257,7 +258,7 @@ def test_include_dir_merge_named_recursive(mock_walk):
with io.StringIO(conf) as file:
assert '.ignore' in mock_walk.return_value[0][1], \
"Expecting .ignore in here"
doc = yaml.yaml.safe_load(file)
doc = yaml_loader.yaml.safe_load(file)
assert 'tmp2' in mock_walk.return_value[0][1]
assert '.ignore' not in mock_walk.return_value[0][1]
assert doc["key"] == {
@ -268,12 +269,12 @@ def test_include_dir_merge_named_recursive(mock_walk):
}
@patch('homeassistant.util.yaml.open', create=True)
@patch('homeassistant.util.yaml.loader.open', create=True)
def test_load_yaml_encoding_error(mock_open):
"""Test raising a UnicodeDecodeError."""
mock_open.side_effect = UnicodeDecodeError('', b'', 1, 0, '')
with pytest.raises(HomeAssistantError):
yaml.load_yaml('test')
yaml_loader.load_yaml('test')
def test_dump():
@ -392,16 +393,16 @@ class TestSecrets(unittest.TestCase):
def test_secrets_keyring(self):
"""Test keyring fallback & get_password."""
yaml.keyring = None # Ensure its not there
yaml_loader.keyring = None # Ensure its not there
yaml_str = 'http:\n api_password: !secret http_pw_keyring'
with pytest.raises(yaml.HomeAssistantError):
with pytest.raises(HomeAssistantError):
load_yaml(self._yaml_path, yaml_str)
yaml.keyring = FakeKeyring({'http_pw_keyring': 'yeah'})
yaml_loader.keyring = FakeKeyring({'http_pw_keyring': 'yeah'})
_yaml = load_yaml(self._yaml_path, yaml_str)
assert {'http': {'api_password': 'yeah'}} == _yaml
@patch.object(yaml, 'credstash')
@patch.object(yaml_loader, 'credstash')
def test_secrets_credstash(self, mock_credstash):
"""Test credstash fallback & get_password."""
mock_credstash.getSecret.return_value = 'yeah'
@ -413,10 +414,10 @@ class TestSecrets(unittest.TestCase):
def test_secrets_logger_removed(self):
"""Ensure logger: debug was removed."""
with pytest.raises(yaml.HomeAssistantError):
with pytest.raises(HomeAssistantError):
load_yaml(self._yaml_path, 'api_password: !secret logger')
@patch('homeassistant.util.yaml._LOGGER.error')
@patch('homeassistant.util.yaml.loader._LOGGER.error')
def test_bad_logger_value(self, mock_error):
"""Ensure logger: debug was removed."""
yaml.clear_secret_cache()