def test_get(self): # You can get a single key. If missing, None or a supplied default is # returned. bag = Bag(c=1, b=2, a=3) self.assertEqual(bag.get('b'), 2) self.assertIsNone(bag.get('missing')) missing = object() self.assertIs(bag.get('missing', missing), missing)
def test_update(self): # Bags can be updated, similar to dicts. bag = Bag(a=1, b=2, c=3) bag.update(b=7, d=9) self.assertEqual(bag.a, 1) self.assertEqual(bag.b, 7) self.assertEqual(bag.c, 3) self.assertEqual(bag.d, 9)
def _parse_device_mappings(device_mapping): devices = {} # e.g. keys: nexus7, nexus4 for device_name, mapping_1 in device_mapping.items(): # Most of the keys at this level (e.g. index) have flat values, # however the keyring key is itself a mapping. keyring = mapping_1.pop('keyring', None) if keyring is not None: mapping_1['keyring'] = Bag(**keyring) # e.g. nexus7 -> {index, keyring} devices[device_name] = Bag(**mapping_1) return Bag(**devices)
def test_pickle(self): # Bags can be pickled and unpickled. bag = Bag(a=1, b=2, c=3) pck = pickle.dumps(bag) new_bag = pickle.loads(pck) self.assertEqual(new_bag.a, 1) self.assertEqual(new_bag.b, 2) self.assertEqual(new_bag.c, 3)
def from_json(cls, data): """Parse the JSON data and produce an index.""" mapping = json.loads(data) # Parse the global data, of which there is only the timestamp. Even # though the string will contain 'UTC' (which we assert is so since we # can only handle UTC timestamps), strptime() will return a naive # datetime. We'll turn it into an aware datetime in UTC, which is the # only thing that can possibly make sense. timestamp_str = mapping['global']['generated_at'] assert 'UTC' in timestamp_str.split(), 'timestamps must be UTC' naive_generated_at = datetime.strptime(timestamp_str, IN_FMT) generated_at = naive_generated_at.replace(tzinfo=timezone.utc) global_ = Bag(generated_at=generated_at) # Parse the images. images = [] for image_data in mapping['images']: # Descriptions can be any of: # # * description # * description-xx (e.g. description-en) # * description-xx_CC (e.g. description-en_US) # # We want to preserve the keys exactly as given, and because the # extended forms are not Python identifiers, we'll pull these out # into a separate, non-Bag dictionary. descriptions = {} # We're going to mutate the dictionary during iteration. for key in list(image_data): if key.startswith('description'): descriptions[key] = image_data.pop(key) files = image_data.pop('files', []) bundles = [Bag(**bundle_data) for bundle_data in files] image = Image(files=bundles, descriptions=descriptions, **image_data) images.append(image) return cls(global_=global_, images=images)
def from_json(cls, data): mapping = json.loads(data) channels = {} for channel_name, mapping_1 in mapping.items(): hidden = mapping_1.pop('hidden', None) if hidden is None: hidden = False else: assert hidden in (True, False), ( "Unexpected value for 'hidden': {}".format(hidden)) mapping_1['hidden'] = hidden device_mapping = mapping_1.pop('devices') mapping_1['devices'] = _parse_device_mappings(device_mapping) channels[channel_name] = Bag(**mapping_1) return cls(**channels)
def test_update_converter_overrides(self): # Converters in the update method permanently override ctor converters. converters = dict(a=int, b=int) bag = Bag(converters=converters, a='1', b='2') self.assertEqual(bag.a, 1) self.assertEqual(bag.b, 2) new_converters = dict(a=str) bag.update(converters=new_converters, a='3', b='4') self.assertEqual(bag.a, '3') self.assertEqual(bag.b, 4) bag.update(a='5', b='6') self.assertEqual(bag.a, '5') self.assertEqual(bag.b, 6)
def test_dash_translation(self): # Dashes in keys get turned into underscore in attributes. bag = Bag(**{'a-b': 1, 'c-d': 2, 'e-f': 3}) self.assertEqual(bag.a_b, 1) self.assertEqual(bag.c_d, 2) self.assertEqual(bag.e_f, 3)
def test_converters(self): # The Bag ctor accepts a mapping of type converter functions. bag = Bag(converters=dict(a=int, b=int), a='1', b='2', c='3') self.assertEqual(bag.a, 1) self.assertEqual(bag.b, 2) self.assertEqual(bag.c, '3')
def test_add_new_key(self): # A key added by setitem can be changed. bag = Bag(a=1, b=2, c=3) bag['d'] = 4 bag['d'] = 5 self.assertEqual(bag.d, 5)
def test_add_existing_key(self): # A key set in the original ctor cannot be changed. bag = Bag(a=1, b=2, c=3) self.assertRaises(ValueError, setitem, bag, 'b', 5) self.assertEqual(bag.b, 2)
def test_add_key(self): # We can add new keys/attributes via setitem. bag = Bag(a=1, b=2, c=3) bag['d'] = bag.b + bag.c self.assertEqual(bag.d, 5)
def test_original(self): # There's a magical attribute containing the original ctor arguments. source = {'a-b': 1, 'global': 2, 'foo': 3} bag = Bag(**source) self.assertEqual(bag.__original__, source)
def test_repr(self): # The repr of a bag includes its translated keys. bag = Bag(**{'a-b': 1, 'global': 2, 'foo': 3}) self.assertEqual(repr(bag), '<Bag: a_b, foo, global_>')
def test_keyword_translation(self): # Python keywords get a trailing underscore. bag = Bag(**{'global': 1, 'with': 2, 'import': 3}) self.assertEqual(bag.global_, 1) self.assertEqual(bag.with_, 2) self.assertEqual(bag.import_, 3)
def _set_defaults(self): self.service = Bag( base='system-image.ubports.com', http_port=80, https_port=443, channel='daily', build_number=0, ) self.system = Bag( timeout=as_timedelta('1h'), tempdir='/tmp', logfile='/var/log/system-image/client.log', loglevel=as_loglevel('info'), settings_db='/var/lib/system-image/settings.db', ) self.gpg = Bag( archive_master='/usr/share/system-image/archive-master.tar.xz', image_master='/var/lib/system-image/keyrings/image-master.tar.xz', image_signing='/var/lib/system-image/keyrings/image-signing.tar.xz', device_signing= '/var/lib/system-image/keyrings/device-signing.tar.xz', ) self.updater = Bag( cache_partition='/android/cache/recovery', data_partition='/var/lib/system-image', ) self.hooks = Bag( device=as_object('systemimage.device.SystemProperty'), scorer=as_object('systemimage.scores.WeightedScorer'), apply=as_object('systemimage.apply.Reboot'), ) self.dbus = Bag(lifetime=as_timedelta('10m'), )
def test_update_converters(self): # The update method also accepts converters. bag = Bag(a=1, b=2, c=3) bag.update(converters=dict(d=int), d='4', e='5') self.assertEqual(bag.d, 4) self.assertEqual(bag.e, '5')
def test_simple(self): # Initialize a bag; its attributes are the keywords of the ctor. bag = Bag(a=1, b=2, c=3) self.assertEqual(bag.a, 1) self.assertEqual(bag.b, 2) self.assertEqual(bag.c, 3)
def test_iter(self): # Iteration is over the available keys. bag = Bag(c=1, b=2, a=3) self.assertEqual(sorted(bag, reverse=True), ['c', 'b', 'a'])
def test_keys(self): bag = Bag(c=1, b=2, a=3) self.assertEqual(sorted(bag.keys()), ['a', 'b', 'c'])
def test_dash_literal_access(self): # For keys with dashes, the original name is preserved in getitem. bag = Bag(**{'a-b': 1, 'c-d': 2, 'e-f': 3}) self.assertEqual(bag['a-b'], 1) self.assertEqual(bag['c-d'], 2) self.assertEqual(bag['e-f'], 3)
class Configuration: def __init__(self, directory=None): self._set_defaults() # Because the configuration object is a global singleton, it makes for # a convenient place to stash information used by widely separate # components. For example, this is a placeholder for rendezvous # between the downloader and the D-Bus service. When running under # D-Bus and we get a `paused` signal from the download manager, we need # this to plumb through an UpdatePaused signal to our clients. It # rather sucks that we need a global for this, but I can't get the # plumbing to work otherwise. This seems like the least horrible place # to stash this global. self.dbus_service = None # These are used to plumb command line arguments from the main() to # other parts of the system. self.skip_gpg_verification = False self.override_gsm = False # Cache. self._device = None self._build_number = None self.build_number_override = False self._channel = None # This is used only to override the phased percentage via command line # and the property setter. self._phase_override = None self._tempdir = None self.config_d = None self.ini_files = [] self.http_base = None self.https_base = None if directory is not None: self.load(directory) self._calculate_http_bases() self._resources = ExitStack() atexit.register(self._resources.close) def _set_defaults(self): self.service = Bag( base='system-image.ubports.com', http_port=80, https_port=443, channel='daily', build_number=0, ) self.system = Bag( timeout=as_timedelta('1h'), tempdir='/tmp', logfile='/var/log/system-image/client.log', loglevel=as_loglevel('info'), settings_db='/var/lib/system-image/settings.db', ) self.gpg = Bag( archive_master='/usr/share/system-image/archive-master.tar.xz', image_master='/var/lib/system-image/keyrings/image-master.tar.xz', image_signing='/var/lib/system-image/keyrings/image-signing.tar.xz', device_signing= '/var/lib/system-image/keyrings/device-signing.tar.xz', ) self.updater = Bag( cache_partition='/android/cache/recovery', data_partition='/var/lib/system-image', ) self.hooks = Bag( device=as_object('systemimage.device.SystemProperty'), scorer=as_object('systemimage.scores.WeightedScorer'), apply=as_object('systemimage.apply.Reboot'), ) self.dbus = Bag(lifetime=as_timedelta('10m'), ) def _load_file(self, path): parser = SafeConfigParser() str_path = str(path) parser.read(str_path) self.ini_files.append(path) self.service.update(converters=dict( http_port=as_port, https_port=as_port, build_number=int, device=as_stripped, ), **parser['service']) self.system.update(converters=dict(timeout=as_timedelta, loglevel=as_loglevel, settings_db=expand_path, tempdir=expand_path), **parser['system']) self.gpg.update(**parser['gpg']) self.updater.update(**parser['updater']) self.hooks.update(converters=dict(device=as_object, scorer=as_object, apply=as_object), **parser['hooks']) self.dbus.update(converters=dict(lifetime=as_timedelta), **parser['dbus']) def load(self, directory): """Load up the configuration from a config.d directory.""" # Look for all the files in the given directory with .ini or .cfg # suffixes. The files must start with a number, and the files are # loaded in numeric order. if self.config_d is not None: raise RuntimeError('Configuration already loaded; use .reload()') self.config_d = directory if not Path(directory).is_dir(): raise TypeError( '.load() requires a directory: {}'.format(directory)) candidates = [] for child in Path(directory).glob('*.ini'): order, _, base = child.stem.partition('_') # XXX 2014-10-03: The logging system isn't initialized when we get # here, so we can't log that these files are being ignored. if len(_) == 0: continue try: serial = int(order) except ValueError: continue candidates.append((serial, child)) for serial, path in sorted(candidates): self._load_file(path) self._calculate_http_bases() def reload(self): """Reload the configuration directory.""" # Reset some cached attributes. directory = self.config_d self.ini_files = [] self.config_d = None self._build_number = None # Now load the defaults, then reload the previous config.d directory. self._set_defaults() self.load(directory) def _calculate_http_bases(self): if (self.service.http_port is NO_PORT and self.service.https_port is NO_PORT): raise ValueError('Cannot disable both http and https ports') # Construct the HTTP and HTTPS base urls, which most applications will # actually use. We do this in two steps, in order to support disabling # one or the other (but not both) protocols. if self.service.http_port == 80: http_base = 'http://{}'.format(self.service.base) elif self.service.http_port is NO_PORT: http_base = None else: http_base = 'http://{}:{}'.format(self.service.base, self.service.http_port) # HTTPS. if self.service.https_port == 443: https_base = 'https://{}'.format(self.service.base) elif self.service.https_port is NO_PORT: https_base = None else: https_base = 'https://{}:{}'.format(self.service.base, self.service.https_port) # Sanity check and final settings. if http_base is None: assert https_base is not None http_base = https_base if https_base is None: assert http_base is not None https_base = http_base self.http_base = http_base self.https_base = https_base @property def build_number(self): if self._build_number is None: self._build_number = self.service.build_number return self._build_number @build_number.setter def build_number(self, value): if not isinstance(value, int): raise ValueError('integer is required, got: {}'.format( type(value).__name__)) self._build_number = value self.build_number_override = True @build_number.deleter def build_number(self): self._build_number = None @property def device(self): if self._device is None: # Start by looking for a [service]device setting. Use this if it # exists, otherwise fall back to calling the hook. self._device = getattr(self.service, 'device', None) if not self._device: self._device = self.hooks.device().get_device() return self._device @device.setter def device(self, value): self._device = value @property def channel(self): if self._channel is None: self._channel = self.service.channel return self._channel @channel.setter def channel(self, value): self._channel = value @property def phase_override(self): return self._phase_override @phase_override.setter def phase_override(self, value): self._phase_override = max(0, min(100, int(value))) @phase_override.deleter def phase_override(self): self._phase_override = None @property def tempdir(self): if self._tempdir is None: makedirs(self.system.tempdir) self._tempdir = self._resources.enter_context( temporary_directory(prefix='system-image-', dir=self.system.tempdir)) return self._tempdir @property def user_agent(self): return USER_AGENT.format(self)