Merge pull request #981 from balloob/ordered-yaml

Load YAML config into an ordered dict
pull/970/merge
Paulus Schoutsen 2016-01-23 23:04:41 -08:00
commit e541b9ba77
20 changed files with 163 additions and 139 deletions

View File

@ -223,7 +223,7 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
enable_logging(hass, verbose, daemon, log_rotate_days) enable_logging(hass, verbose, daemon, log_rotate_days)
config_dict = config_util.load_config_file(config_path) config_dict = config_util.load_yaml_config_file(config_path)
return from_config_dict(config_dict, hass, enable_log=False, return from_config_dict(config_dict, hass, enable_log=False,
skip_pip=skip_pip) skip_pip=skip_pip)

View File

@ -16,8 +16,8 @@ import itertools as it
import logging import logging
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.util as util from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers import extract_entity_ids from homeassistant.helpers.service import extract_entity_ids
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE)
@ -36,7 +36,7 @@ def is_on(hass, entity_id=None):
entity_ids = hass.states.entity_ids() entity_ids = hass.states.entity_ids()
for entity_id in entity_ids: for entity_id in entity_ids:
domain = util.split_entity_id(entity_id)[0] domain = split_entity_id(entity_id)[0]
module = get_component(domain) module = get_component(domain)
@ -92,7 +92,7 @@ def setup(hass, config):
# Group entity_ids by domain. groupby requires sorted data. # Group entity_ids by domain. groupby requires sorted data.
by_domain = it.groupby(sorted(entity_ids), by_domain = it.groupby(sorted(entity_ids),
lambda item: util.split_entity_id(item)[0]) lambda item: split_entity_id(item)[0])
for domain, ent_ids in by_domain: for domain, ent_ids in by_domain:
# We want to block for all calls and only return when all calls # We want to block for all calls and only return when all calls

View File

@ -11,7 +11,7 @@ the user has submitted configuration information.
""" """
import logging import logging
from homeassistant.helpers import generate_entity_id from homeassistant.helpers.entity import generate_entity_id
from homeassistant.const import EVENT_TIME_CHANGED from homeassistant.const import EVENT_TIME_CHANGED
DOMAIN = "configurator" DOMAIN = "configurator"

View File

@ -7,10 +7,9 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/group/ https://home-assistant.io/components/group/
""" """
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.helpers import generate_entity_id
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import (
import homeassistant.util as util Entity, split_entity_id, generate_entity_id)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED,
@ -62,7 +61,7 @@ def expand_entity_ids(hass, entity_ids):
try: try:
# If entity_id points at a group, expand it # If entity_id points at a group, expand it
domain, _ = util.split_entity_id(entity_id) domain, _ = split_entity_id(entity_id)
if domain == DOMAIN: if domain == DOMAIN:
found_ids.extend( found_ids.extend(
@ -75,7 +74,7 @@ def expand_entity_ids(hass, entity_ids):
found_ids.append(entity_id) found_ids.append(entity_id)
except AttributeError: except AttributeError:
# Raised by util.split_entity_id if entity_id is not a string # Raised by split_entity_id if entity_id is not a string
pass pass
return found_ids return found_ids

View File

@ -14,10 +14,9 @@ from homeassistant.core import State, DOMAIN as HA_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_ON, STATE_OFF, EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_ON, STATE_OFF,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
from homeassistant import util
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun from homeassistant.components import recorder, sun
from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook" DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http'] DEPENDENCIES = ['recorder', 'http']
@ -209,7 +208,7 @@ def humanify(events):
entity_id = event.data.get(ATTR_ENTITY_ID) entity_id = event.data.get(ATTR_ENTITY_ID)
if domain is None and entity_id is not None: if domain is None and entity_id is not None:
try: try:
domain = util.split_entity_id(str(entity_id))[0] domain = split_entity_id(str(entity_id))[0]
except IndexError: except IndexError:
pass pass

View File

@ -13,10 +13,10 @@ from itertools import islice
import threading import threading
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity, split_entity_id
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.service import call_from_config from homeassistant.helpers.service import call_from_config
from homeassistant.util import slugify, split_entity_id from homeassistant.util import slugify
import homeassistant.util.dt as date_util import homeassistant.util.dt as date_util
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, EVENT_TIME_CHANGED, STATE_ON, SERVICE_TURN_ON, ATTR_ENTITY_ID, EVENT_TIME_CHANGED, STATE_ON, SERVICE_TURN_ON,

View File

@ -10,8 +10,8 @@ import logging
from homeassistant.const import ( from homeassistant.const import (
ATTR_HIDDEN, ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME) ATTR_HIDDEN, ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME)
from homeassistant.helpers import extract_domain_configs, generate_entity_id from homeassistant.helpers import extract_domain_configs
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity, generate_entity_id
from homeassistant.util.location import distance from homeassistant.util.location import distance
DOMAIN = "zone" DOMAIN = "zone"

View File

@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME,
CONF_TIME_ZONE) CONF_TIME_ZONE)
import homeassistant.util.location as loc_util import homeassistant.util.location as loc_util
from homeassistant.util.yaml import load_yaml
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -113,40 +114,9 @@ def find_config_file(config_dir):
return config_path if os.path.isfile(config_path) else None return config_path if os.path.isfile(config_path) else None
def load_config_file(config_path):
""" Loads given config file. """
return load_yaml_config_file(config_path)
def load_yaml_config_file(config_path): def load_yaml_config_file(config_path):
""" Parse a YAML configuration file. """ """ Parse a YAML configuration file. """
import yaml conf_dict = load_yaml(config_path)
def parse(fname):
""" Parse 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) or {}
except yaml.YAMLError:
error = 'Error reading YAML configuration file {}'.format(fname)
_LOGGER.exception(error)
raise HomeAssistantError(error)
def yaml_include(loader, node):
"""
Loads another YAML file and embeds it using the !include tag.
Example:
device_tracker: !include device_tracker.yaml
"""
fname = os.path.join(os.path.dirname(loader.name), node.value)
return parse(fname)
yaml.add_constructor('!include', yaml_include)
conf_dict = parse(config_path)
if not isinstance(conf_dict, dict): if not isinstance(conf_dict, dict):
_LOGGER.error( _LOGGER.error(

View File

@ -11,7 +11,6 @@ import logging
import signal import signal
import threading import threading
import enum import enum
import re
import functools as ft import functools as ft
from collections import namedtuple from collections import namedtuple
@ -26,6 +25,7 @@ from homeassistant.exceptions import (
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
import homeassistant.util.location as location import homeassistant.util.location as location
from homeassistant.helpers.entity import valid_entity_id, split_entity_id
import homeassistant.helpers.temperature as temp_helper import homeassistant.helpers.temperature as temp_helper
from homeassistant.config import get_default_config_dir from homeassistant.config import get_default_config_dir
@ -42,9 +42,6 @@ SERVICE_CALL_LIMIT = 10 # seconds
# will be added for each component that polls devices. # will be added for each component that polls devices.
MIN_WORKER_THREAD = 2 MIN_WORKER_THREAD = 2
# Pattern for validating entity IDs (format: <domain>.<entity>)
ENTITY_ID_PATTERN = re.compile(r"^(?P<domain>\w+)\.(?P<entity>\w+)$")
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Temporary to support deprecated methods # Temporary to support deprecated methods
@ -339,7 +336,7 @@ class State(object):
def __init__(self, entity_id, state, attributes=None, last_changed=None, def __init__(self, entity_id, state, attributes=None, last_changed=None,
last_updated=None): last_updated=None):
"""Initialize a new state.""" """Initialize a new state."""
if not ENTITY_ID_PATTERN.match(entity_id): if not valid_entity_id(entity_id):
raise InvalidEntityFormatError(( raise InvalidEntityFormatError((
"Invalid entity id encountered: {}. " "Invalid entity id encountered: {}. "
"Format should be <domain>.<object_id>").format(entity_id)) "Format should be <domain>.<object_id>").format(entity_id))
@ -360,12 +357,12 @@ class State(object):
@property @property
def domain(self): def domain(self):
"""Domain of this state.""" """Domain of this state."""
return util.split_entity_id(self.entity_id)[0] return split_entity_id(self.entity_id)[0]
@property @property
def object_id(self): def object_id(self):
"""Object id of this state.""" """Object id of this state."""
return util.split_entity_id(self.entity_id)[1] return split_entity_id(self.entity_id)[1]
@property @property
def name(self): def name(self):

View File

@ -3,42 +3,7 @@ Helper methods for components within Home Assistant.
""" """
import re import re
from homeassistant.loader import get_component from homeassistant.const import CONF_PLATFORM
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
from homeassistant.util import ensure_unique_string, slugify
def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
""" Generate a unique entity ID based on given entity IDs or used ids. """
name = name.lower() or DEVICE_DEFAULT_NAME.lower()
if current_ids is None:
if hass is None:
raise RuntimeError("Missing required parameter currentids or hass")
current_ids = hass.states.entity_ids()
return ensure_unique_string(
entity_id_format.format(slugify(name.lower())), current_ids)
def extract_entity_ids(hass, service):
"""
Helper method to extract a list of entity ids from a service call.
Will convert group entity ids to the entity ids it represents.
"""
if not (service.data and ATTR_ENTITY_ID in service.data):
return []
group = get_component('group')
# Entity ID attr can be a list or a string
service_ent_id = service.data[ATTR_ENTITY_ID]
if isinstance(service_ent_id, str):
return group.expand_entity_ids(hass, [service_ent_id])
return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)]
def validate_config(config, items, logger): def validate_config(config, items, logger):

View File

@ -6,8 +6,10 @@ Provides ABC for entities in HA.
""" """
from collections import defaultdict from collections import defaultdict
import re
from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.exceptions import NoEntitySpecifiedError
from homeassistant.util import ensure_unique_string, slugify
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON,
@ -17,6 +19,32 @@ from homeassistant.const import (
# Dict mapping entity_id to a boolean that overwrites the hidden property # Dict mapping entity_id to a boolean that overwrites the hidden property
_OVERWRITE = defaultdict(dict) _OVERWRITE = defaultdict(dict)
# Pattern for validating entity IDs (format: <domain>.<entity>)
ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$")
def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
""" Generate a unique entity ID based on given entity IDs or used ids. """
name = name.lower() or DEVICE_DEFAULT_NAME.lower()
if current_ids is None:
if hass is None:
raise RuntimeError("Missing required parameter currentids or hass")
current_ids = hass.states.entity_ids()
return ensure_unique_string(
entity_id_format.format(slugify(name.lower())), current_ids)
def split_entity_id(entity_id):
""" Splits a state entity_id into domain, object_id. """
return entity_id.split(".", 1)
def valid_entity_id(entity_id):
"""Test if an entity ID is a valid format."""
return ENTITY_ID_PATTERN.match(entity_id) is not None
class Entity(object): class Entity(object):
""" ABC for Home Assistant entities. """ """ ABC for Home Assistant entities. """

View File

@ -7,9 +7,10 @@ Provides helpers for components that manage entities.
from threading import Lock from threading import Lock
from homeassistant.bootstrap import prepare_setup_platform from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.helpers import ( from homeassistant.helpers import config_per_platform
generate_entity_id, config_per_platform, extract_entity_ids) from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
from homeassistant.helpers.service import extract_entity_ids
from homeassistant.components import group, discovery from homeassistant.components import group, discovery
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID

View File

@ -1,8 +1,9 @@
"""Service calling related helpers.""" """Service calling related helpers."""
import logging import logging
from homeassistant.util import split_entity_id
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.helpers.entity import split_entity_id
from homeassistant.loader import get_component
CONF_SERVICE = 'service' CONF_SERVICE = 'service'
CONF_SERVICE_ENTITY_ID = 'entity_id' CONF_SERVICE_ENTITY_ID = 'entity_id'
@ -41,3 +42,22 @@ def call_from_config(hass, config, blocking=False):
service_data[ATTR_ENTITY_ID] = entity_id service_data[ATTR_ENTITY_ID] = entity_id
hass.services.call(domain, service, service_data, blocking) hass.services.call(domain, service, service_data, blocking)
def extract_entity_ids(hass, service):
"""
Helper method to extract a list of entity ids from a service call.
Will convert group entity ids to the entity ids it represents.
"""
if not (service.data and ATTR_ENTITY_ID in service.data):
return []
group = get_component('group')
# Entity ID attr can be a list or a string
service_ent_id = service.data[ATTR_ENTITY_ID]
if isinstance(service_ent_id, str):
return group.expand_entity_ids(hass, [service_ent_id])
return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)]

View File

@ -41,11 +41,6 @@ def slugify(text):
return RE_SLUGIFY.sub("", text) return RE_SLUGIFY.sub("", text)
def split_entity_id(entity_id):
""" Splits a state entity_id into domain, object_id. """
return entity_id.split(".", 1)
def repr_helper(inp): def repr_helper(inp):
""" Helps creating a more readable string representation of objects. """ """ Helps creating a more readable string representation of objects. """
if isinstance(inp, dict): if isinstance(inp, dict):

View File

@ -0,0 +1,50 @@
"""
YAML utility functions.
"""
from collections import OrderedDict
import logging
import os
import yaml
from homeassistant.exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
def load_yaml(fname):
"""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) or {}
except yaml.YAMLError:
error = 'Error reading YAML configuration file {}'.format(fname)
_LOGGER.exception(error)
raise HomeAssistantError(error)
def _include_yaml(loader, node):
"""
Loads another YAML file and embeds it using the !include tag.
Example:
device_tracker: !include device_tracker.yaml
"""
fname = os.path.join(os.path.dirname(loader.name), node.value)
return load_yaml(fname)
def _ordered_dict(loader, node):
"""
Loads YAML mappings into an ordered dict to preserve key order.
"""
loader.flatten_mapping(node)
return OrderedDict(loader.construct_pairs(node))
yaml.add_constructor('!include', _include_yaml)
yaml.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
_ordered_dict)

View File

@ -42,3 +42,8 @@ class TestHelpersEntity(unittest.TestCase):
state = self.hass.states.get(self.entity.entity_id) state = self.hass.states.get(self.entity.entity_id)
self.assertTrue(state.attributes.get(ATTR_HIDDEN)) self.assertTrue(state.attributes.get(ATTR_HIDDEN))
def test_split_entity_id(self):
""" Test split_entity_id. """
self.assertEqual(['domain', 'object_id'],
entity.split_entity_id('domain.object_id'))

View File

@ -7,45 +7,22 @@ Tests component helpers.
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import unittest import unittest
import homeassistant.core as ha from homeassistant import helpers
from homeassistant import loader, helpers
from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID
from tests.common import get_test_home_assistant from tests.common import get_test_home_assistant
class TestComponentsCore(unittest.TestCase): class TestHelpers(unittest.TestCase):
""" Tests homeassistant.components module. """ """ Tests homeassistant.helpers module. """
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
""" Init needed objects. """ """ Init needed objects. """
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.hass.states.set('light.Bowl', STATE_ON)
self.hass.states.set('light.Ceiling', STATE_OFF)
self.hass.states.set('light.Kitchen', STATE_OFF)
loader.get_component('group').setup_group(
self.hass, 'test', ['light.Ceiling', 'light.Kitchen'])
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass.stop() self.hass.stop()
def test_extract_entity_ids(self):
""" Test extract_entity_ids method. """
call = ha.ServiceCall('light', 'turn_on',
{ATTR_ENTITY_ID: 'light.Bowl'})
self.assertEqual(['light.bowl'],
helpers.extract_entity_ids(self.hass, call))
call = ha.ServiceCall('light', 'turn_on',
{ATTR_ENTITY_ID: 'group.test'})
self.assertEqual(['light.ceiling', 'light.kitchen'],
helpers.extract_entity_ids(self.hass, call))
def test_extract_domain_configs(self): def test_extract_domain_configs(self):
config = { config = {
'zone': None, 'zone': None,

View File

@ -7,7 +7,8 @@ Test service helpers.
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from homeassistant.const import SERVICE_TURN_ON from homeassistant import core as ha, loader
from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID
from homeassistant.helpers import service from homeassistant.helpers import service
from tests.common import get_test_home_assistant, mock_service from tests.common import get_test_home_assistant, mock_service
@ -66,3 +67,24 @@ class TestServiceHelpers(unittest.TestCase):
'service': 'invalid' 'service': 'invalid'
}) })
self.assertEqual(3, mock_log.call_count) self.assertEqual(3, mock_log.call_count)
def test_extract_entity_ids(self):
""" Test extract_entity_ids method. """
self.hass.states.set('light.Bowl', STATE_ON)
self.hass.states.set('light.Ceiling', STATE_OFF)
self.hass.states.set('light.Kitchen', STATE_OFF)
loader.get_component('group').setup_group(
self.hass, 'test', ['light.Ceiling', 'light.Kitchen'])
call = ha.ServiceCall('light', 'turn_on',
{ATTR_ENTITY_ID: 'light.Bowl'})
self.assertEqual(['light.bowl'],
service.extract_entity_ids(self.hass, call))
call = ha.ServiceCall('light', 'turn_on',
{ATTR_ENTITY_ID: 'group.test'})
self.assertEqual(['light.ceiling', 'light.kitchen'],
service.extract_entity_ids(self.hass, call))

View File

@ -94,13 +94,14 @@ class TestConfig(unittest.TestCase):
with self.assertRaises(HomeAssistantError): with self.assertRaises(HomeAssistantError):
config_util.load_yaml_config_file(YAML_PATH) config_util.load_yaml_config_file(YAML_PATH)
def test_load_config_loads_yaml_config(self): def test_load_yaml_config_preserves_key_order(self):
""" Test correct YAML config loading. """
with open(YAML_PATH, 'w') as f: with open(YAML_PATH, 'w') as f:
f.write('hello: world') f.write('hello: 0\n')
f.write('world: 1\n')
self.assertEqual({'hello': 'world'}, self.assertEqual(
config_util.load_config_file(YAML_PATH)) [('hello', 0), ('world', 1)],
list(config_util.load_yaml_config_file(YAML_PATH).items()))
@mock.patch('homeassistant.util.location.detect_location_info', @mock.patch('homeassistant.util.location.detect_location_info',
mock_detect_location_info) mock_detect_location_info)
@ -109,7 +110,7 @@ class TestConfig(unittest.TestCase):
""" Test that detect location sets the correct config keys. """ """ Test that detect location sets the correct config keys. """
config_util.ensure_config_exists(CONFIG_DIR) config_util.ensure_config_exists(CONFIG_DIR)
config = config_util.load_config_file(YAML_PATH) config = config_util.load_yaml_config_file(YAML_PATH)
self.assertIn(DOMAIN, config) self.assertIn(DOMAIN, config)

View File

@ -36,11 +36,6 @@ class TestUtil(unittest.TestCase):
self.assertEqual("test_more", util.slugify("Test More")) self.assertEqual("test_more", util.slugify("Test More"))
self.assertEqual("test_more", util.slugify("Test_(More)")) self.assertEqual("test_more", util.slugify("Test_(More)"))
def test_split_entity_id(self):
""" Test split_entity_id. """
self.assertEqual(['domain', 'object_id'],
util.split_entity_id('domain.object_id'))
def test_repr_helper(self): def test_repr_helper(self):
""" Test repr_helper. """ """ Test repr_helper. """
self.assertEqual("A", util.repr_helper("A")) self.assertEqual("A", util.repr_helper("A"))