def test_remove_colors(self): """Test our ANSI/ECMA REGEXP colors removal method""" self.assertFalse(Colors.remove_colors(str(Colors.CLEAR))) self.assertEqual(Colors.remove_colors('\x1b[0;31mTEST\x1b[0;0m'), 'TEST') self.assertEqual(Colors.remove_colors('\x1b[0nTEST\xde\xad\xbe\xaf'), '\x1b[0nTEST\xde\xad\xbe\xaf')
def test_get_level_color(self): """Test for `Colors.get_level_color`""" # [25] (GREEN) < 50 < 75 self.assertEqual(Colors.get_level_color(25, 50, 75), Colors.GREEN_NORMAL) # 33 < [34] (YELLOW) < 66 self.assertEqual(Colors.get_level_color(34, 33, 66), Colors.YELLOW_NORMAL) # 33 < 66 < [90] (RED) self.assertEqual(Colors.get_level_color(90, 33, 66), Colors.RED_NORMAL)
def _get_colors_palette(self): """Build and return a 8-color palette, with Unicode characters if allowed""" # On systems with non-Unicode locales, we imitate '\u2588' character # ... with '#' to display the terminal colors palette. use_unicode = self._configuration.get('colors_palette')['use_unicode'] return ' '.join([ '{normal}{character}{bright}{character}{clear}'.format( normal=Colors((0, i)), bright=Colors((1, i)), character=('\u2588' if use_unicode else '#'), clear=Colors.CLEAR) for i in range(37, 30, -1) ])
def _get_colors_palette(self) -> str: """Build and return a 8-color palette, with Unicode characters if allowed""" # On systems with non-Unicode locales, we imitate '\u2588' character # ... with '#' to display the terminal colors palette. # Archey >= v4.8.0, Unicode is enabled by default. use_unicode = self.options.get('use_unicode', True) return ' '.join([ '{normal}{character}{bright}{character}{clear}'.format( normal=Colors((0, i)), bright=Colors((1, i)), character=('\u2588' if use_unicode else '#'), clear=Colors.CLEAR) for i in range(37, 30, -1) ])
def __init__(self, **kwargs): # Fetches passed arguments. self._format_to_json = kwargs.get('format_to_json') try: # If set, force the distribution to `preferred_distribution` argument. self._distribution = Distributions( kwargs.get('preferred_distribution')) except ValueError: # If not (or unknown), run distribution detection. self._distribution = Distributions.run_detection() # Fetch the colors palette related to this distribution. self._colors_palette = COLORS_DICT[self._distribution] # If `os-release`'s `ANSI_COLOR` option is set, honor it. ansi_color = Distributions.get_ansi_color() if ansi_color and Configuration().get( 'colors_palette')['honor_ansi_color']: # Replace each Archey integrated colors by `ANSI_COLOR`. self._colors_palette = len(self._colors_palette) * \ [Colors.escape_code_from_attrs(ansi_color)] # Each entry will be added to this list self._entries = [] # Each class output will be added in the list below afterwards self._results = []
def __init__(self, **kwargs): # Fetches passed arguments. self._format_to_json = kwargs.get('format_to_json') # type: int try: # If set, force the distribution to `preferred_distribution` argument. self._distribution = Distributions(kwargs.get('preferred_distribution')) except ValueError: # If not (or unknown), run distribution detection. self._distribution = Distributions.run_detection() # Retrieve distribution's logo module before copying and DRY-ing its attributes. logo_module = lazy_load_logo_module(self._distribution.value) self._logo, self._colors = logo_module.LOGO.copy(), logo_module.COLORS.copy() # If `os-release`'s `ANSI_COLOR` option is set, honor it. ansi_color = Distributions.get_ansi_color() if ansi_color and Configuration().get('honor_ansi_color'): # Replace each Archey integrated colors by `ANSI_COLOR`. self._colors = len(self._colors) * [Colors.escape_code_from_attrs(ansi_color)] # Each entry will be added to this list self._entries = [] # Each class output will be added in the list below afterwards self._results = []
def __init__(self): # The configuration object is needed to retrieve some settings below. configuration = Configuration() # This dictionary will store values obtained from sub-processes calls. self._usage = {'used': 0.0, 'total': 0.0} self._run_df_usage() self._run_btrfs_usage() # Check whether at least one media could be found. if not self._usage['total']: self.value = configuration.get('default_strings')['not_detected'] return # Fetch the user-defined disk limits from configuration. disk_limits = configuration.get('limits')['disk'] # Based on the disk percentage usage, select the corresponding level color. level_color = Colors.get_level_color( (self._usage['used'] / (self._usage['total'] or 1)) * 100, disk_limits['warning'], disk_limits['danger']) self.value = '{0}{1} GiB{2} / {3} GiB'.format( level_color, round(self._usage['used'], 1), Colors.CLEAR, round(self._usage['total'], 1))
def output(self, output): """ Adds the entry to `output` after pretty-formatting the RAM usage with color and units. """ if not self.value: # Fall back on the default behavior if no RAM usage could be detected. super().output(output) return # DRY some constants used = self.value['used'] total = self.value['total'] unit = self.value['unit'] # Fetch the user-defined RAM limits from configuration. ram_limits = self._configuration.get('limits')['ram'] # Based on the RAM percentage usage, select the corresponding level color. level_color = Colors.get_level_color( (used / total) * 100, ram_limits['warning'], ram_limits['danger']) output.append( self.name, '{0}{1} {unit}{2} / {3} {unit}'.format(level_color, int(used), Colors.CLEAR, int(total), unit=unit))
def output(self, output): """ Adds the entry to `output` after pretty-formatting the RAM usage with color and units. """ if not self.value: # Fall back on the default behavior if no RAM usage could be detected. super().output(output) return # DRY some constants used = self.value['used'] total = self.value['total'] unit = self.value['unit'] # Based on the RAM percentage usage, select the corresponding level color. level_color = Colors.get_level_color( (used / total) * 100, self.options.get('warning_use_percent', 33.3), self.options.get('danger_use_percent', 66.7) ) output.append( self.name, f'{level_color}{int(used)} {unit}{Colors.CLEAR} / {int(total)} {unit}' )
def output(self, output): """Adds the entry to `output` after pretty-formatting with colors palette""" text_output = (self.value or self._default_strings.get('not_detected')) if Colors.should_color_output(): text_output += ' ' + self._get_colors_palette() output.append(self.name, text_output)
def test_distribution_logos_no_empty_lines(self): """Check that distribution logos do not contain (useless) empty lines""" for distribution in Distributions: for i, line in enumerate(LOGOS_DICT[distribution]): self.assertTrue( Colors.remove_colors(line).strip(), msg='[{0}] line index {1}, got a forbidden empty line'. format(distribution, i))
def __init__(self, **kwargs): # Fetches passed arguments. self._format_to_json = kwargs.get('format_to_json') preferred_logo_style = (kwargs.get('preferred_logo_style') or '').upper() try: # If set, force the distribution to `preferred_distribution` argument. self._distribution = Distributions( kwargs.get('preferred_distribution')) except ValueError: # If not (or unknown), run distribution detection. self._distribution = Distributions.get_local() # Retrieve distribution's logo module before copying and DRY-ing its attributes. logo_module = lazy_load_logo_module(self._distribution.value) # If set and available, fetch an alternative logo style from module. if preferred_logo_style and hasattr(logo_module, f"LOGO_{preferred_logo_style}"): self._logo = getattr(logo_module, f"LOGO_{preferred_logo_style}").copy() self._colors = getattr(logo_module, f"COLORS_{preferred_logo_style}").copy() else: self._logo, self._colors = logo_module.LOGO.copy( ), logo_module.COLORS.copy() configuration = Configuration() # If `os-release`'s `ANSI_COLOR` option is set, honor it. ansi_color = Distributions.get_ansi_color() if ansi_color and configuration.get("honor_ansi_color"): # Replace each Archey integrated colors by `ANSI_COLOR`. self._colors = len( self._colors) * [Colors.escape_code_from_attrs(ansi_color)] entries_color = configuration.get("entries_color") self._entries_color = (Colors.escape_code_from_attrs(entries_color) if entries_color else self._colors[0]) # Each entry will be added to this list self._entries = [] # Each class output will be added in the list below afterwards self._results = []
def __init__(self): # The configuration object is needed to retrieve some settings below. configuration = Configuration() terminal = os.getenv( 'TERM', configuration.get('default_strings')['not_detected']) # On systems with non-Unicode locales, we imitate '\u2588' character # ... with '#' to display the terminal colors palette. # This is the default option for backward compatibility. use_unicode = configuration.get('colors_palette')['use_unicode'] colors = ' '.join([ '{normal}{character}{bright}{character}{clear}'.format( normal=Colors((0, i)), bright=Colors((1, i)), character=('\u2588' if use_unicode else '#'), clear=Colors.CLEAR) for i in range(37, 30, -1) ]) self.value = '{0} {1}'.format(terminal, colors)
def test_ansi_ecma_regexp(self): """Test our ANSI/ECMA REGEXP compiled pattern""" self.assertTrue(ANSI_ECMA_REGEXP.match(str(Colors.CLEAR))) self.assertTrue(ANSI_ECMA_REGEXP.match(str(Colors.RED_NORMAL))) self.assertTrue( ANSI_ECMA_REGEXP.match(Colors.escape_code_from_attrs('0;31;45'))) self.assertFalse(ANSI_ECMA_REGEXP.match('')) self.assertFalse(ANSI_ECMA_REGEXP.match('\x1b[m')) self.assertFalse(ANSI_ECMA_REGEXP.match('\x1b[0M')) # Check that matched groups contain the whole code (no capturing groups). self.assertEqual( len(''.join( ANSI_ECMA_REGEXP.findall( str(Colors.GREEN_NORMAL) + 'NOT_A_COLOR' + str(Colors.CLEAR)))), len(str(Colors.GREEN_NORMAL) + str(Colors.CLEAR)))
def __init__(self): # Fetch the user-defined RAM limits from configuration. ram_limits = Configuration().get('limits')['ram'] try: ram = ''.join( filter( re.compile('Mem').search, check_output( ['free', '-m'], env={'LANG': 'C'}, universal_newlines=True ).splitlines() ) ).split() used = float(ram[2]) total = float(ram[1]) except (IndexError, FileNotFoundError): # An in-digest one-liner to retrieve memory info into a dictionary with open('/proc/meminfo') as file: ram = { i.split(':')[0]: float(i.split(':')[1].strip(' kB')) / 1024 for i in filter(None, file.read().splitlines()) } total = ram['MemTotal'] # Here, let's imitate Neofetch's behavior. # See <https://github.com/dylanaraps/neofetch/wiki/Frequently-Asked-Questions>. used = total + ram['Shmem'] - ( ram['MemFree'] + ram['Cached'] + ram['SReclaimable'] + ram['Buffers']) # Imitates what `free` does when the obtained value happens to be incorrect. # See <https://gitlab.com/procps-ng/procps/blob/master/proc/sysinfo.c#L790>. if used < 0: used = total - ram['MemFree'] # Based on the RAM percentage usage, select the corresponding level color. level_color = Colors.get_level_color( (used / total) * 100, ram_limits['warning'], ram_limits['danger'] ) self.value = '{0}{1} MiB{2} / {3} MiB'.format( level_color, int(used), Colors.CLEAR, int(total) )
def output(self, output) -> None: if not self.value: # Fall back on the default behavior if load average values could not be detected. super().output(output) return # DRY constant thresholds. warning_threshold = self.options.get("warning_threshold", 1.0) danger_threshold = self.options.get("danger_threshold", 2.0) output.append( self.name, " ".join([ str( Colors.get_level_color(load_avg, warning_threshold, danger_threshold)) + str(load_avg) + str(Colors.CLEAR) for load_avg in self.value ]), )
def test_should_color_output(self): """Test for `Colors.should_color_output`""" # Clear cache filled by `functools.lru_cache` decorator. Colors.should_color_output.cache_clear() with patch('archey.colors.Environment.CLICOLOR_FORCE', True): self.assertTrue(Colors.should_color_output()) Colors.should_color_output.cache_clear() with patch('archey.colors.Environment.CLICOLOR_FORCE', False), \ patch('archey.colors.Environment.NO_COLOR', True): self.assertFalse(Colors.should_color_output()) Colors.should_color_output.cache_clear() with patch('archey.colors.Environment.CLICOLOR_FORCE', False), \ patch('archey.colors.Environment.NO_COLOR', False): with patch('archey.colors.sys.stdout.isatty', return_value=False): with patch('archey.colors.Environment.CLICOLOR', True): self.assertFalse(Colors.should_color_output()) Colors.should_color_output.cache_clear() with patch('archey.colors.Environment.CLICOLOR', False): self.assertFalse(Colors.should_color_output()) Colors.should_color_output.cache_clear() with patch('archey.colors.sys.stdout.isatty', return_value=True): # Default case : STDOUT is a TTY and `CLICOLOR` is (by default) set. with patch('archey.colors.Environment.CLICOLOR', True): self.assertTrue(Colors.should_color_output()) Colors.should_color_output.cache_clear() with patch('archey.colors.Environment.CLICOLOR', False): self.assertFalse(Colors.should_color_output()) Colors.should_color_output.cache_clear()
def test_escape_code_from_attrs(self): """Test for `Colors.escape_code_from_attrs`""" self.assertEqual(Colors.escape_code_from_attrs('0;31'), '\x1b[0;31m') self.assertEqual(Colors.escape_code_from_attrs('0;31;45'), '\x1b[0;31;45m')
def test_distribution_logos_consistency(self): """ Verify each distribution identifier got a logo module. Verify each distribution logo module contain `LOGO` & `COLORS` ("truthy") attributes. Also check they got _consistent_ widths across their respective lines. Additionally verify they don't contain any (useless) empty line. This test also indirectly checks `lazy_load_logo_module` behavior! """ distributions_identifiers = Distributions.get_distribution_identifiers( ) for i, logo_module_info in enumerate(pkgutil.iter_modules( logos.__path__), start=1): # `iter_modules` yields `pkgutil.ModuleInfo` named tuple starting with Python 3.6. # So we manually extract the module name from `(module_finder, name, ispkg)` tuple. logo_module_name = logo_module_info[1] # Check each logo module name corresponds to a distribution identifier. self.assertIn(logo_module_name, distributions_identifiers, msg='No distribution identifier for [{0}]'.format( logo_module_name)) logo_module = lazy_load_logo_module(logo_module_name) # Attributes checks. self.assertTrue( getattr(logo_module, 'LOGO', []), msg='[{0}] logo module missing `LOGO` attribute'.format( logo_module_name)) self.assertTrue( getattr(logo_module, 'COLORS', []), msg='[{0}] logo module missing `COLORS` attribute'.format( logo_module_name)) # Make Archey compute the logo width. logo_width = get_logo_width(logo_module.LOGO) # Then, check that each logo line got the same effective width. for j, line in enumerate(logo_module.LOGO[1:], start=1): # Here we gotta trick the `get_logo_width` call. # We actually pass each logo line as if it was a "complete" logo. line_width = get_logo_width([line]) # Width check. self.assertEqual( line_width, logo_width, msg= '[{0}] line index {1}, got an unexpected width {2} (expected {3})' .format(logo_module_name, j, line_width, logo_width)) # Non-empty line check. self.assertTrue( Colors.remove_colors(line).strip(), msg='[{0}] line index {1}, got an useless empty line'. format(logo_module_name, j)) # Finally, check each distributions identifier got a logo! # pylint: disable=undefined-loop-variable self.assertEqual(i, len(distributions_identifiers), msg='[{0}] Expected {1} logo modules, got {2}'.format( logo_module_name, len(distributions_identifiers), i))
def _output_text(self): """ Finally render the output entries. It handles text centering additionally to value and colors replacing. """ # Let's copy the logo (so we don't modify the constant!) logo = LOGOS_DICT[self._distribution].copy() logo_width = get_logo_width(logo, len(self._colors_palette)) # Let's center the entries and the logo (handles odd numbers) height_diff = len(logo) - len(self._results) if height_diff >= 0: self._results[0:0] = [''] * (height_diff // 2) self._results.extend([''] * (len(logo) - len(self._results))) else: colored_empty_line = [ str(self._colors_palette[0]) + ' ' * logo_width ] logo[0:0] = colored_empty_line * (-height_diff // 2) logo.extend(colored_empty_line * (len(self._results) - len(logo))) text_wrapper = TextWrapper(width=(get_terminal_size().columns - logo_width), expand_tabs=False, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False, max_lines=1, placeholder='...') placeholder_length = len(text_wrapper.placeholder) # Using `TextWrapper`, shortens each entry to remove any line overlapping for i, entry in enumerate(self._results): # Shortens the entry according to the terminal width. # We have to remove any ANSI color, or the result would be skewed. wrapped_entry = text_wrapper.fill(Colors.remove_colors(entry)) placeholder_offset = (placeholder_length if wrapped_entry.endswith( text_wrapper.placeholder) else 0) # By using previous positions, re-inserts ANSI colors back in the wrapped string. for color_match in ANSI_ECMA_REGEXP.finditer(entry): match_index = color_match.start() if match_index <= len(wrapped_entry) - placeholder_offset: wrapped_entry = (wrapped_entry[:match_index] + color_match.group() + wrapped_entry[match_index:]) # Add a color reset character before the placeholder (if any). # Rationale : # We cannot set `Colors.CLEAR` in the placeholder as it would skew its internals. if placeholder_offset: wrapped_entry = (wrapped_entry[:-placeholder_length] + str(Colors.CLEAR) + wrapped_entry[-placeholder_length:]) self._results[i] = wrapped_entry # Merge entry results to the distribution logo. logo_with_entries = os.linesep.join([ logo_part + entry_part for logo_part, entry_part in zip(logo, self._results) ]) try: print( logo_with_entries.format(c=self._colors_palette) + str(Colors.CLEAR)) except UnicodeError: print("""\ Your locale or TTY does not seem to support UTF-8 encoding. Please disable Unicode within your configuration file.\ """, file=sys.stderr)
def test_distribution_logos_consistency(self): """ Verify each distribution identifier got a logo module. Verify each distribution logo module contain `LOGO` & `COLORS` ("truthy") attributes. Also check they got _consistent_ widths across their respective lines. Additionally verify they don't contain any (useless) empty line. This test also indirectly checks `lazy_load_logo_module` behavior! """ distributions_identifiers = Distributions.get_identifiers() for i, logo_module_info in enumerate(pkgutil.iter_modules( logos.__path__), start=1): # Check each logo module name corresponds to a distribution identifier. self.assertIn( logo_module_info.name, distributions_identifiers, msg=f'No distribution identifier for [{logo_module_info.name}]' ) logo_module = lazy_load_logo_module(logo_module_info.name) # Attributes checks. self.assertTrue( getattr(logo_module, 'LOGO', []), msg= f'[{logo_module_info.name}] logo module missing `LOGO` attribute' ) self.assertTrue( getattr(logo_module, 'COLORS', []), msg= f'[{logo_module_info.name}] logo module missing `COLORS` attribute' ) # Compute once and for all the number of defined colors for this logo. nb_colors = len(logo_module.COLORS) # Make Archey compute the logo (effective) width. logo_width = get_logo_width(logo_module.LOGO, nb_colors) # Then, check that each logo line got the same effective width. for j, line in enumerate(logo_module.LOGO[1:], start=1): # Here we gotta trick the `get_logo_width` call. # We actually pass each logo line as if it was a "complete" logo. line_width = get_logo_width([line], nb_colors) # Width check. self.assertEqual( line_width, logo_width, msg= '[{}] line index {}, got an unexpected width {} (expected {})' .format(logo_module_info.name, j, line_width, logo_width)) # Non-empty line check. self.assertTrue( Colors.remove_colors(line.format(c=[''] * nb_colors)).strip(), msg= f'[{logo_module_info.name}] line index {j}, got an useless empty line' ) # Finally, check each distributions identifier got a logo! # pylint: disable=undefined-loop-variable self.assertEqual(i, len(distributions_identifiers), msg='[{}] Expected {} logo modules, got {}'.format( logo_module_info.name, len(distributions_identifiers), i))
def output(self, output): """ Adds the entry to `output` after formatting with color and units. Follows the user configuration supplied for formatting. """ # Fetch our `filesystems` object locally so we can modify it safely. filesystems = self.value if not filesystems: # We didn't find any disk, fall back to the default entry behavior. super().output(output) return # DRY configuration object for the output. disk_labels = self.options.get('disk_labels') hide_entry_name = self.options.get('hide_entry_name') # Combine all entries into one grand-total if configured to do so. if self.options.get('combine_total', True): name = self.name # Rewrite our `filesystems` object as one combining all of them. filesystems = { None: { 'device_path': None, 'used_blocks': sum([ filesystem_data['used_blocks'] for filesystem_data in filesystems.values() ]), 'total_blocks': sum([ filesystem_data['total_blocks'] for filesystem_data in filesystems.values() ]) } } else: # We will only use disk labels and entry name hiding if we aren't combining entries. name = '' # Hide `Disk` from entry name only if the user specified it... as long as a label. if not hide_entry_name or not disk_labels: name += self.name if disk_labels: if not hide_entry_name: name += ' ' name += '({disk_label})' # We will only run this loop a single time for combined entries. for mount_point, filesystem_data in filesystems.items(): # Select the corresponding level color based on disk percentage usage. level_color = Colors.get_level_color( (filesystem_data['used_blocks'] / filesystem_data['total_blocks']) * 100, self.options.get('warning_use_percent', 50), self.options.get('danger_use_percent', 75)) # Set the correct disk label if disk_labels == 'mount_points': disk_label = mount_point elif disk_labels == 'device_paths': disk_label = filesystem_data['device_path'] else: disk_label = None pretty_filesystem_value = f'{level_color}{{}}{Colors.CLEAR} / {{}}'.format( self._blocks_to_human_readable(filesystem_data['used_blocks']), self._blocks_to_human_readable( filesystem_data['total_blocks'])) output.append(name.format(disk_label=disk_label), pretty_filesystem_value)
def test_constant_values(self): """Test enumeration member instantiation from value""" self.assertEqual(Colors((1, 31)), Colors.RED_BRIGHT) self.assertRaises(ValueError, Colors, (-1, ))
def _output_text(self) -> None: """ Finally render the output entries. It handles text centering additionally to value and colors replacing. """ # Compute the effective logo "width" from the loaded ASCII art. logo_width = get_logo_width(self._logo, len(self._colors)) # Let's center the entries and the logo (handles odd numbers) height_diff = len(self._logo) - len(self._results) if height_diff >= 0: self._results[0:0] = [''] * (height_diff // 2) self._results.extend([''] * (len(self._logo) - len(self._results))) else: colored_empty_line = [str(self._colors[0]) + ' ' * logo_width] self._logo[0:0] = colored_empty_line * (-height_diff // 2) self._logo.extend(colored_empty_line * (len(self._results) - len(self._logo))) # When writing to a pipe (for instance), prevent `TextWrapper` from truncating output. if not sys.stdout.isatty(): text_width = cast(int, float("inf")) else: text_width = get_terminal_size().columns - logo_width - len( self.__LOGO_RIGHT_PADDING) text_wrapper = TextWrapper(width=text_width, expand_tabs=False, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False, max_lines=1, placeholder='...') placeholder_length = len(text_wrapper.placeholder) # Using `TextWrapper`, shortens each entry to remove any line overlapping for i, entry in enumerate(self._results): # Shortens the entry according to the terminal width. # We have to remove any ANSI color, or the result would be skewed. wrapped_entry = text_wrapper.fill(Colors.remove_colors(entry)) placeholder_offset = (placeholder_length if wrapped_entry.endswith( text_wrapper.placeholder) else 0) # By using previous positions, re-inserts ANSI colors back in the wrapped string. for color_match in ANSI_ECMA_REGEXP.finditer(entry): match_index = color_match.start() if match_index <= len(wrapped_entry) - placeholder_offset: wrapped_entry = (wrapped_entry[:match_index] + color_match.group() + wrapped_entry[match_index:]) # Add a color reset character before the placeholder (if any). # Rationale : # We cannot set `Colors.CLEAR` in the placeholder as it would skew its internals. if placeholder_offset: wrapped_entry = (wrapped_entry[:-placeholder_length] + str(Colors.CLEAR) + wrapped_entry[-placeholder_length:]) self._results[i] = wrapped_entry # Merge entry results to the distribution logo. logo_with_entries = os.linesep.join([ f"{logo_part}{self.__LOGO_RIGHT_PADDING}{entry_part}" for logo_part, entry_part in zip(self._logo, self._results) ]) try: print(logo_with_entries.format(c=self._colors) + str(Colors.CLEAR)) except UnicodeError as unicode_error: raise ArcheyException("""\ Your locale or TTY does not seem to support UTF-8 encoding. Please disable Unicode within your configuration file.\ """) from unicode_error