def enable_ansi_support(): """ Try to enable support for ANSI escape sequences (required on Windows). :returns: :data:`True` if ANSI is supported, :data:`False` otherwise. This functions checks for the following supported configurations, in the given order: 1. On Windows, if :func:`have_windows_native_ansi_support()` confirms native support for ANSI escape sequences :mod:`ctypes` will be used to enable this support. 2. On Windows, if the environment variable ``$ANSICON`` is set nothing is done because it is assumed that support for ANSI escape sequences has already been enabled via `ansicon <https://github.com/adoxa/ansicon>`_. 3. On Windows, an attempt is made to import and initialize the Python package :pypi:`colorama` instead (of course for this to work :pypi:`colorama` has to be installed). 4. On other platforms this function calls :func:`connected_to_terminal()` to determine whether ANSI escape sequences are supported (that is to say all platforms that are not Windows are assumed to support ANSI escape sequences natively, without weird contortions like above). This makes it possible to call :func:`enable_ansi_support()` unconditionally without checking the current platform. The :func:`~humanfriendly.decorators.cached` decorator is used to ensure that this function is only executed once, but its return value remains available on later calls. """ if have_windows_native_ansi_support(): import ctypes ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7) ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-12), 7) return True elif on_windows(): if 'ANSICON' in os.environ: return True try: import colorama colorama.init() return True except ImportError: return False else: return connected_to_terminal()
def is_syslog_supported(): """ Determine whether system logging is supported. :returns: :data:`True` if system logging is supported and can be enabled, :data:`False` if system logging is not supported or there are good reasons for not enabling it. The decision making process here is as follows: Override If the environment variable ``$COLOREDLOGS_SYSLOG`` is set it is evaluated using :func:`~humanfriendly.coerce_boolean()` and the resulting value overrides the platform detection discussed below, this allows users to override the decision making process if they disagree / know better. Linux / UNIX On systems that are not Windows or MacOS (see below) we assume UNIX which means either syslog is available or sending a bunch of UDP packets to nowhere won't hurt anyone... Microsoft Windows Over the years I've had multiple reports of :pypi:`coloredlogs` spewing extremely verbose errno 10057 warning messages to the console (once for each log message I suppose) so I now assume it a default that "syslog-style system logging" is not generally available on Windows. Apple MacOS There's cPython issue `#38780`_ which seems to result in a fatal exception when the Python interpreter shuts down. This is (way) worse than not having system logging enabled. The error message mentioned in `#38780`_ has actually been following me around for years now, see for example: - https://github.com/xolox/python-rotate-backups/issues/9 mentions Docker images implying Linux, so not strictly the same as `#38780`_. - https://github.com/xolox/python-npm-accel/issues/4 is definitely related to `#38780`_ and is what eventually prompted me to add the :func:`is_syslog_supported()` logic. .. _#38780: https://bugs.python.org/issue38780 """ override = os.environ.get("COLOREDLOGS_SYSLOG") if override is not None: return coerce_boolean(override) else: return not (on_windows() or on_macos())
def have_windows_native_ansi_support(): """ Check if we're running on a Windows 10 release with native support for ANSI escape sequences. :returns: :data:`True` if so, :data:`False` otherwise. The :func:`~humanfriendly.decorators.cached` decorator is used as a minor performance optimization. Semantically this should have zero impact because the answer doesn't change in the lifetime of a computer process. """ if on_windows(): try: # I can't be 100% sure this will never break and I'm not in a # position to test it thoroughly either, so I decided that paying # the price of one additional try / except statement is worth the # additional peace of mind :-). components = tuple(int(c) for c in platform.version().split('.')) return components >= (10, 0, 14393) except Exception: pass return False
def use_color(): use_colors = True if use_colors or (use_colors is None): # Respect the user's choice not to have colors. if use_colors is None and 'NO_COLOR' in os.environ: # For details on this see https://no-color.org/. use_colors = False # Try to enable Windows native ANSI support or Colorama? if (use_colors or use_colors is None) and on_windows(): # This can fail, in which case ANSI escape sequences would end # up being printed to the terminal in raw form. This is very # user hostile, so to avoid this happening we disable color # support on failure. use_colors = enable_ansi_support() # When auto detection is enabled, and so far we encountered no # reason to disable color support, then we will enable color # support if 'stream' is connected to a terminal. if use_colors is None: use_colors = terminal_supports_colors() return use_colors
def terminal_supports_colors(stream=None): """ Check if a stream is connected to a terminal that supports ANSI escape sequences. :param stream: The stream to check (a file-like object, defaults to :data:`sys.stdout`). :returns: :data:`True` if the terminal supports ANSI escape sequences, :data:`False` otherwise. This function was originally inspired by the implementation of `django.core.management.color.supports_color() <https://github.com/django/django/blob/master/django/core/management/color.py>`_ but has since evolved significantly. """ if on_windows(): # On Windows support for ANSI escape sequences is not a given. have_ansicon = 'ANSICON' in os.environ have_colorama = 'colorama' in sys.modules have_native_support = have_windows_native_ansi_support() if not (have_ansicon or have_colorama or have_native_support): return False return connected_to_terminal(stream)
def main(): """Command line interface for the ``rotate-backups`` program.""" coloredlogs.install() # Command line option defaults. rotation_scheme = {} kw = dict(include_list=[], exclude_list=[]) parallel = False use_sudo = False use_syslog = (not on_windows()) # Internal state. selected_locations = [] # Parse the command line arguments. try: options, arguments = getopt.getopt( sys.argv[1:], 'M:H:d:w:m:y:t:I:x:jpri:c:C:uS:fnvqh', [ 'minutely=', 'hourly=', 'daily=', 'weekly=', 'monthly=', 'yearly=', 'timestamp-pattern=', 'include=', 'exclude=', 'parallel', 'prefer-recent', 'relaxed', 'ionice=', 'config=', 'removal-command=', 'use-sudo', 'syslog=', 'force', 'dry-run', 'verbose', 'quiet', 'help', ]) for option, value in options: if option in ('-M', '--minutely'): rotation_scheme['minutely'] = coerce_retention_period(value) elif option in ('-H', '--hourly'): rotation_scheme['hourly'] = coerce_retention_period(value) elif option in ('-d', '--daily'): rotation_scheme['daily'] = coerce_retention_period(value) elif option in ('-w', '--weekly'): rotation_scheme['weekly'] = coerce_retention_period(value) elif option in ('-m', '--monthly'): rotation_scheme['monthly'] = coerce_retention_period(value) elif option in ('-y', '--yearly'): rotation_scheme['yearly'] = coerce_retention_period(value) elif option in ('-t', '--timestamp-pattern'): kw['timestamp_pattern'] = value elif option in ('-I', '--include'): kw['include_list'].append(value) elif option in ('-x', '--exclude'): kw['exclude_list'].append(value) elif option in ('-j', '--parallel'): parallel = True elif option in ('-p', '--prefer-recent'): kw['prefer_recent'] = True elif option in ('-r', '--relaxed'): kw['strict'] = False elif option in ('-i', '--ionice'): value = validate_ionice_class(value.lower().strip()) kw['io_scheduling_class'] = value elif option in ('-c', '--config'): kw['config_file'] = parse_path(value) elif option in ('-C', '--removal-command'): removal_command = shlex.split(value) logger.info("Using custom removal command: %s", removal_command) kw['removal_command'] = removal_command elif option in ('-u', '--use-sudo'): use_sudo = True elif option in ('-S', '--syslog'): use_syslog = coerce_boolean(value) elif option in ('-f', '--force'): kw['force'] = True elif option in ('-n', '--dry-run'): logger.info("Performing a dry run (because of %s option) ..", option) kw['dry_run'] = True elif option in ('-v', '--verbose'): coloredlogs.increase_verbosity() elif option in ('-q', '--quiet'): coloredlogs.decrease_verbosity() elif option in ('-h', '--help'): usage(__doc__) return else: assert False, "Unhandled option! (programming error)" if use_syslog: enable_system_logging() if rotation_scheme: logger.verbose("Rotation scheme defined on command line: %s", rotation_scheme) if arguments: # Rotation of the locations given on the command line. location_source = 'command line arguments' selected_locations.extend( coerce_location(value, sudo=use_sudo) for value in arguments) else: # Rotation of all configured locations. location_source = 'configuration file' selected_locations.extend( location for location, rotation_scheme, options in load_config_file( configuration_file=kw.get('config_file'), expand=True)) # Inform the user which location(s) will be rotated. if selected_locations: logger.verbose("Selected %s based on %s:", pluralize(len(selected_locations), "location"), location_source) for number, location in enumerate(selected_locations, start=1): logger.verbose(" %i. %s", number, location) else: # Show the usage message when no directories are given nor configured. logger.verbose("No location(s) to rotate selected.") usage(__doc__) return except Exception as e: logger.error("%s", e) sys.exit(1) # Rotate the backups in the selected directories. program = RotateBackups(rotation_scheme, **kw) if parallel: program.rotate_concurrent(*selected_locations) else: for location in selected_locations: program.rotate_backups(location)