def patch_clean_configuration(method_definition: Callable = None, *, configuration: dict = None) -> Callable: """ Decorator for an entry test definition, which sets the entry's `_default_strings` attribute to the Archey defaults, optionally updated with `configuration`. """ # Let's initially give the entry configuration the defaults. # We deep-copy `DEFAULT_CONFIG` to prevent its mutation. entry_configuration = deepcopy(DEFAULT_CONFIG) # Then, let's merge in `configuration` recursively. Utility.update_recursive(entry_configuration, (configuration or {})) def decorator_patch_clean_configuration(method: Callable) -> Callable: @wraps(method) def wrapper_patch_clean_configuration(*args, **kwargs): with patch('archey.entry.Configuration', autospec=True) as config_instance_mock: # Mock "publicly" used methods. config_instance_mock().get = entry_configuration.get config_instance_mock().__iter__ = iter( entry_configuration.items()) return method(*args, **kwargs) return wrapper_patch_clean_configuration if method_definition is None: return decorator_patch_clean_configuration return decorator_patch_clean_configuration(method_definition)
def entry_mock(entry, options: dict = None, configuration: dict = None) -> MagicMock: """ Creates a placeholder "instance" of the entry class passed, with a clean default `_default_strings` which is optionally updated by `configuration`. It can be used to very cleanly unit-test instance methods of a class, by passing it in (after setting appropriate attributes). The attributes defined are not instance attributes, however since this isn't technically an instance, they are used in place of the respective instance attributes. """ # We spec to the entry so non-existent methods can't be called... # ...and wrap it, to inherit its methods. instance_mock = MagicMock(spec=entry, wraps=entry) # These instance-attributes are quite important, so let's mimic them. instance_mock.name = getattr(entry, '_PRETTY_NAME') or str(entry.__name__) instance_mock.value = None # (entry default) # We don't have default entry options defined outside of entries. instance_mock.options = options or {} # Let's initially give the entry configuration the defaults. # We deep-copy `DEFAULT_CONFIG` to prevent its mutation. default_configuration = deepcopy(DEFAULT_CONFIG) # Then, let's merge in `configuration` recursively. Utility.update_recursive(default_configuration, (configuration or {})) # Replaces the internal (and protected!) `_default_strings` attribute by... # ... the corresponding object from configuration. setattr(instance_mock, '_default_strings', default_configuration.get('default_strings')) # Finally provisions a proper `logging.Logger` instance for our mock. setattr(instance_mock, '_logger', logging.getLogger(entry.__module__)) return instance_mock
def _load_configuration(self, path: str) -> None: """ A method handling configuration loading from a JSON file. It will try to load any `config.json` present under `path`. """ # If a previous configuration file has denied overriding... if not self.get('allow_overriding'): # ... don't load this one. return # If the specified `path` is a directory, append the file name we are looking for. if os.path.isdir(path): path = os.path.join(path, 'config.json') try: with open(path, mode='rb') as f_config: Utility.update_recursive(self._config, json.load(f_config)) self._config_files_info[path] = os.fstat(f_config.fileno()) except FileNotFoundError: return except json.JSONDecodeError as json_decode_error: logging.error('%s (%s)', json_decode_error, path) return # When `suppress_warnings` is set, higher the log level to silence warning messages. logging.getLogger().setLevel( logging.ERROR if self.get('suppress_warnings') else logging.WARN)
def test_version_to_semver_segments(self): """Check `version_to_semver_segments` implementation""" self.assertTupleEqual(Utility.version_to_semver_segments('v1.2.3'), (1, 2, 3)) self.assertTupleEqual( Utility.version_to_semver_segments('1.2.3.4-beta5'), (1, 2, 3, 4)) self.assertTupleEqual(Utility.version_to_semver_segments('1'), (1, ))
def _load_configuration(self, path: str): """ A method handling configuration loading from a JSON file. It will try to load any `config.json` present under `path`. """ # If a previous configuration file has denied overriding... if not self.get('allow_overriding'): # ... don't load this one. return # If the specified `path` is a directory, append the file name we are looking for. if os.path.isdir(path): path = os.path.join(path, 'config.json') try: with open(path) as f_config: Utility.update_recursive(self._config, json.load(f_config)) except FileNotFoundError: return except json.JSONDecodeError as json_decode_error: print('Warning: {0} ({1})'.format(json_decode_error, path), file=sys.stderr) return # If the user does not want any warning to appear : 2> /dev/null if self.get('suppress_warnings'): # One more if statement to avoid multiple `open` calls. if sys.stderr == self._stderr: sys.stderr = open(os.devnull, 'w') else: self._close_and_restore_sys_stderr()
def patch_clean_configuration( method_definition: Callable = None, *, configuration: dict = None ) -> Callable: """ Decorator for an entry test definition, which sets the entry's `_default_strings` attribute to the Archey defaults, optionally updated with `configuration`. """ # Let's initially give defaults to configuration objects. # We deep-copy `DEFAULT_CONFIG` to prevent its mutation. default_config = deepcopy(DEFAULT_CONFIG) # Then we recursively merge in passed `configuration`. Utility.update_recursive(default_config, (configuration or {})) def decorator_patch_clean_configuration(method: Callable) -> Callable: @wraps(method) def wrapper_patch_clean_configuration(*args, **kwargs): # `Configuration` singleton is used in `Entry` and `Output` unit-tested modules. with patch( "archey.entry.Configuration", autospec=True ) as entry_config_instance_mock, patch( "archey.output.Configuration", autospec=True ) as output_config_instance_mock: # Mock "publicly" used methods. entry_config_instance_mock().get = default_config.get entry_config_instance_mock().__iter__ = iter(default_config.items()) output_config_instance_mock().get = default_config.get output_config_instance_mock().__iter__ = iter(default_config.items()) return method(*args, **kwargs) return wrapper_patch_clean_configuration if method_definition is None: return decorator_patch_clean_configuration return decorator_patch_clean_configuration(method_definition)
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.value = { 'name': platform.system(), 'release': platform.release(), 'latest': None, 'is_outdated': None } # On GNU/Linux systems, if `check_version` has been enabled and `DO_NOT_TRACK` isn't set, # retrieve the latest kernel release in order to compare the current one against it. if not self.options.get('check_version') \ or self.value['name'] != 'Linux' \ or Environment.DO_NOT_TRACK: return self.value['latest'] = self._fetch_latest_linux_release() if self.value['latest']: self.value['is_outdated'] = (Utility.version_to_semver_segments( self.value['release']) < Utility.version_to_semver_segments( self.value['latest']))
def json_serialization(self, indent: int = 0) -> str: """ JSON serialization of entries. Set `indent` to the number of wanted output indentation tabs (2-space long). """ document = { 'data': {entry.name: entry.value for entry in self.entries}, 'meta': { 'version': Utility.version_to_semver_segments(__version__), 'date': datetime.now().isoformat(), 'count': len(self.entries), 'distro': Distributions.get_local().value, } } return json.dumps(document, indent=((indent * 2) or None))
def json_serialization(self, indent: int = 0) -> str: """ JSON serialization of entries. Set `indent` to the number of wanted output indentation tabs (2-space long). Note: For Python < 3.6, the keys order is not guaranteed. """ document = { 'data': {entry.name: entry.value for entry in self.entries}, 'meta': { 'version': Utility.version_to_semver_segments(__version__), 'date': datetime.now().isoformat(), 'count': len(self.entries) } } return json.dumps(document, indent=((indent * 2) or None))
def test_update_recursive(self): """Test for the `update_recursive` class method""" test_dict = { 'allow_overriding': True, 'suppress_warnings': False, 'default_strings': { 'no_address': 'No Address', 'not_detected': 'Not detected' }, 'colors_palette': { 'use_unicode': False }, 'ip_settings': { 'lan_ip_max_count': 2 }, 'temperature': { 'use_fahrenheit': False } } # We change existing values, add new ones, and omit some others. Utility.update_recursive( test_dict, { 'suppress_warnings': True, 'colors_palette': { 'use_unicode': False }, 'default_strings': { 'no_address': '\xde\xad \xbe\xef', 'not_detected': 'Not detected', 'virtual_environment': 'Virtual Environment' }, 'temperature': { 'a_weird_new_dict': [None, 'l33t', { 'really': 'one_more_?' }] } }) self.assertDictEqual( test_dict, { 'allow_overriding': True, 'suppress_warnings': True, 'colors_palette': { 'use_unicode': False }, 'default_strings': { 'no_address': '\xde\xad \xbe\xef', 'not_detected': 'Not detected', 'virtual_environment': 'Virtual Environment' }, 'ip_settings': { 'lan_ip_max_count': 2 }, 'temperature': { 'use_fahrenheit': False, 'a_weird_new_dict': [None, 'l33t', { 'really': 'one_more_?' }] } })