Source code for openlp.core.common

# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4

###############################################################################
# OpenLP - Open Source Lyrics Projection                                      #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers                                   #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it     #
# under the terms of the GNU General Public License as published by the Free  #
# Software Foundation; version 2 of the License.                              #
#                                                                             #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
# more details.                                                               #
#                                                                             #
# You should have received a copy of the GNU General Public License along     #
# with this program; if not, write to the Free Software Foundation, Inc., 59  #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
###############################################################################
"""
The :mod:`common` module contains most of the components and libraries that make
OpenLP work.
"""
import hashlib
import importlib
import logging
import os
import re
import sys
import traceback
from chardet.universaldetector import UniversalDetector
from ipaddress import IPv4Address, IPv6Address, AddressValueError
from pathlib import Path
from shutil import which
from subprocess import check_output, CalledProcessError, STDOUT

from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QCryptographicHash as QHash

log = logging.getLogger(__name__ + '.__init__')


FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
IMAGES_FILTER = None


[docs]def trace_error_handler(logger): """ Log the calling path of an exception :param logger: logger to use so traceback is logged to correct class """ log_string = "OpenLP Error trace" for tb in traceback.extract_stack(): log_string += '\n File {file} at line {line} \n\t called {data}'.format(file=tb[0], line=tb[1], data=tb[3]) logger.error(log_string)
[docs]def check_directory_exists(directory, do_not_log=False): """ Check a theme directory exists and if not create it :param directory: The directory to make sure exists :param do_not_log: To not log anything. This is need for the start up, when the log isn't ready. """ if not do_not_log: log.debug('check_directory_exists {text}'.format(text=directory)) try: if not os.path.exists(directory): os.makedirs(directory) except IOError as e: if not do_not_log: log.exception('failed to check if directory exists or create directory')
[docs]def extension_loader(glob_pattern, excluded_files=[]): """ A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and importers. :param glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the application directory. i.e. openlp/plugins/*/*plugin.py :type glob_pattern: str :param excluded_files: A list of file names to exclude that the glob pattern may find. :type excluded_files: list of strings :return: None :rtype: None """ app_dir = Path(AppLocation.get_directory(AppLocation.AppDir)).parent for extension_path in app_dir.glob(glob_pattern): extension_path = extension_path.relative_to(app_dir) if extension_path.name in excluded_files: continue module_name = path_to_module(extension_path) try: importlib.import_module(module_name) except (ImportError, OSError): # On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X) log.warning('Failed to import {module_name} on path {extension_path}' .format(module_name=module_name, extension_path=str(extension_path)))
[docs]def path_to_module(path): """ Convert a path to a module name (i.e openlp.core.common) :param path: The path to convert to a module name. :type path: Path :return: The module name. :rtype: str """ module_path = path.with_suffix('') return '.'.join(module_path.parts)
[docs]def get_frozen_path(frozen_option, non_frozen_option): """ Return a path based on the system status. :param frozen_option: :param non_frozen_option: """ if hasattr(sys, 'frozen') and sys.frozen == 1: return frozen_option return non_frozen_option
[docs]class ThemeLevel(object): """ Provides an enumeration for the level a theme applies to """ Global = 1 Service = 2 Song = 3
[docs]def translate(context, text, comment=None, qt_translate=QtCore.QCoreApplication.translate): """ A special shortcut method to wrap around the Qt5 translation functions. This abstracts the translation procedure so that we can change it if at a later date if necessary, without having to redo the whole of OpenLP. :param context: The translation context, used to give each string a context or a namespace. :param text: The text to put into the translation tables for translation. :param comment: An identifying string for when the same text is used in different roles within the same context. :param qt_translate: """ return qt_translate(context, text, comment)
[docs]class SlideLimits(object): """ Provides an enumeration for behaviour of OpenLP at the end limits of each service item when pressing the up/down arrow keys """ End = 1 Wrap = 2 Next = 3
[docs]def de_hump(name): """ Change any Camel Case string to python string """ sub_name = FIRST_CAMEL_REGEX.sub(r'\1_\2', name) return SECOND_CAMEL_REGEX.sub(r'\1_\2', sub_name).lower()
[docs]def is_win(): """ Returns true if running on a system with a nt kernel e.g. Windows, Wine :return: True if system is running a nt kernel false otherwise """ return os.name.startswith('nt')
[docs]def is_macosx(): """ Returns true if running on a system with a darwin kernel e.g. Mac OS X :return: True if system is running a darwin kernel false otherwise """ return sys.platform.startswith('darwin')
[docs]def is_linux(): """ Returns true if running on a system with a linux kernel e.g. Ubuntu, Debian, etc :return: True if system is running a linux kernel false otherwise """ return sys.platform.startswith('linux')
[docs]def verify_ipv4(addr): """ Validate an IPv4 address :param addr: Address to validate :returns: bool """ try: valid = IPv4Address(addr) return True except AddressValueError: return False
[docs]def verify_ipv6(addr): """ Validate an IPv6 address :param addr: Address to validate :returns: bool """ try: valid = IPv6Address(addr) return True except AddressValueError: return False
[docs]def verify_ip_address(addr): """ Validate an IP address as either IPv4 or IPv6 :param addr: Address to validate :returns: bool """ return True if verify_ipv4(addr) else verify_ipv6(addr)
[docs]def md5_hash(salt=None, data=None): """ Returns the hashed output of md5sum on salt,data using Python3 hashlib :param salt: Initial salt :param data: OPTIONAL Data to hash :returns: str """ log.debug('md5_hash(salt="{text}")'.format(text=salt)) if not salt and not data: return None hash_obj = hashlib.new('md5') if salt: hash_obj.update(salt) if data: hash_obj.update(data) hash_value = hash_obj.hexdigest() log.debug('md5_hash() returning "{text}"'.format(text=hash_value)) return hash_value
[docs]def qmd5_hash(salt=None, data=None): """ Returns the hashed output of MD5Sum on salt, data using PyQt5.QCryptographicHash. Function returns a QByteArray instead of a text string. If you need a string instead, call with result = str(qmd5_hash(salt=..., data=...), encoding='ascii') :param salt: Initial salt :param data: OPTIONAL Data to hash :returns: QByteArray """ log.debug('qmd5_hash(salt="{text}"'.format(text=salt)) if salt is None and data is None: return None hash_obj = QHash(QHash.Md5) if salt: hash_obj.addData(salt) if data: hash_obj.addData(data) hash_value = hash_obj.result().toHex() log.debug('qmd5_hash() returning "{hash}"'.format(hash=hash_value)) return hash_value
[docs]def clean_button_text(button_text): """ Clean the & and other characters out of button text :param button_text: The text to clean """ return button_text.replace('&', '').replace('< ', '').replace(' >', '')
from .openlpmixin import OpenLPMixin from .registry import Registry from .registrymixin import RegistryMixin from .registryproperties import RegistryProperties from .uistrings import UiStrings from .settings import Settings from .applocation import AppLocation from .actions import ActionList from .languagemanager import LanguageManager if is_win(): from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
[docs]def add_actions(target, actions): """ Adds multiple actions to a menu or toolbar in one command. :param target: The menu or toolbar to add actions to :param actions: The actions to be added. An action consisting of the keyword ``None`` will result in a separator being inserted into the target. """ for action in actions: if action is None: target.addSeparator() else: target.addAction(action)
[docs]def get_uno_command(connection_type='pipe'): """ Returns the UNO command to launch an libreoffice.org instance. """ for command in ['libreoffice', 'soffice']: if which(command): break else: raise FileNotFoundError('Command not found') OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard' if connection_type == 'pipe': CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"' else: CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"' return '{cmd} {opt} {conn}'.format(cmd=command, opt=OPTIONS, conn=CONNECTION)
[docs]def get_uno_instance(resolver, connection_type='pipe'): """ Returns a running libreoffice.org instance. :param resolver: The UNO resolver to use to find a running instance. """ log.debug('get UNO Desktop Openoffice - resolve') if connection_type == 'pipe': return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') else: return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
[docs]def get_filesystem_encoding(): """ Returns the name of the encoding used to convert Unicode filenames into system file names. """ encoding = sys.getfilesystemencoding() if encoding is None: encoding = sys.getdefaultencoding() return encoding
[docs]def split_filename(path): """ Return a list of the parts in a given path. """ path = os.path.abspath(path) if not os.path.isfile(path): return path, '' else: return os.path.split(path)
[docs]def delete_file(file_path_name): """ Deletes a file from the system. :param file_path_name: The file, including path, to delete. """ if not file_path_name: return False try: if os.path.exists(file_path_name): os.remove(file_path_name) return True except (IOError, OSError): log.exception("Unable to delete file {text}".format(text=file_path_name)) return False
[docs]def get_images_filter(): """ Returns a filter string for a file dialog containing all the supported image formats. """ global IMAGES_FILTER if not IMAGES_FILTER: log.debug('Generating images filter.') formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats())))) visible_formats = '(*.{text})'.format(text='; *.'.join(formats)) actual_formats = '(*.{text})'.format(text=' *.'.join(formats)) IMAGES_FILTER = '{text} {visible} {actual}'.format(text=translate('OpenLP', 'Image Files'), visible=visible_formats, actual=actual_formats) return IMAGES_FILTER
[docs]def is_not_image_file(file_name): """ Validate that the file is not an image file. :param file_name: File name to be checked. """ if not file_name: return True else: formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()] file_part, file_extension = os.path.splitext(str(file_name)) if file_extension[1:].lower() in formats and os.path.exists(file_name): return False return True
[docs]def clean_filename(filename): """ Removes invalid characters from the given ``filename``. :param filename: The "dirty" file name to clean. """ if not isinstance(filename, str): filename = str(filename, 'utf-8') return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
[docs]def check_binary_exists(program_path): """ Function that checks whether a binary exists. :param program_path: The full path to the binary to check. :return: program output to be parsed """ log.debug('testing program_path: {text}'.format(text=program_path)) try: # Setup startupinfo options for check_output to avoid console popping up on windows if is_win(): startupinfo = STARTUPINFO() startupinfo.dwFlags |= STARTF_USESHOWWINDOW else: startupinfo = None runlog = check_output([program_path, '--help'], stderr=STDOUT, startupinfo=startupinfo) except CalledProcessError as e: runlog = e.output except Exception: trace_error_handler(log) runlog = '' log.debug('check_output returned: {text}'.format(text=runlog)) return runlog
[docs]def get_file_encoding(filename): """ Utility function to incrementally detect the file encoding. :param filename: Filename for the file to determine the encoding for. Str :return: A dict with the keys 'encoding' and 'confidence' """ detector = UniversalDetector() try: with open(filename, 'rb') as detect_file: while not detector.done: chunk = detect_file.read(1024) if not chunk: break detector.feed(chunk) detector.close() return detector.result except OSError: log.exception('Error detecting file encoding')