<commit_before>
__version__ = "0.13.1"
__version_info__ = (0, 12, 0)

import os
import re
import plistlib
from timeit import default_timer as timer

import sublime
import sublime_plugin

if os.getenv('SUBLIME_COLOR_SCHEME_UNIT_DEBUG'):
    def debug_message(message):
        print('DEBUG color_scheme_unit: %s' % str(message))
else:
    def debug_message(message):
        pass

COLOR_TEST_PARAMS_COMPILED_PATTERN = re.compile('^(?:(?:\<\?php )?(?://|#|\/\*|\<\!--)\s*)?COLOR SCHEME TEST "(?P<color_scheme>[^"]+)" "(?P<syntax_name>[^"]+)"(?:\s*(?:--\>|\?\>|\*\/))?')
COLOR_TEST_ASSERTION_COMPILED_PATTERN = re.compile('^(//|#|\/\*|\<\!--)\s*(?P<repeat>\^+)(?: fg=(?P<fg>[^ ]+)?)?(?: bg=(?P<bg>[^ ]+)?)?(?: fs=(?P<fs>[^=]*)?)?$')

class _color_scheme_unit_test_view_set_content(sublime_plugin.TextCommand):

    def run(self, edit, content):
        self.view.erase(edit, sublime.Region(0, self.view.size()))
        self.view.insert(edit, 0, content)

class TestView():

    def __init__(self, name, window):
        self.name = name + '_test_view'
        self.window = window

    def setUp(self):
        self.view = self.window.create_output_panel(self.name, unlisted=True)

    def tearDown(self):
        if self.view:
            self.view.close()

    def set_content(self, content):
        self.view.run_command('_color_scheme_unit_test_view_set_content', {
            'content': content
        })

    def get_content(self):
        return self.view.substr(sublime.Region(0, self.view.size()))

class TestOutputPanel():

    def __init__(self, name, window):
        self.view = window.create_output_panel(name)

        settings = self.view.settings()
        settings.set('result_file_regex', '^(.+):([0-9]+):([0-9]+)$')
        settings.set('word_wrap', False)
        settings.set('line_numbers', False)
        settings.set('gutter', False)
        settings.set('rulers', [])
        settings.set('scroll_past_end', False)

        self.view.assign_syntax('Packages/color_scheme_unit/test_output_panel.sublime-syntax')

        # Assign panel with color scheme of active view
        active_view = window.active_view()
        if active_view:
            active_color_scheme = active_view.settings().get('color_scheme')
            if active_color_scheme:
                settings.set('color_scheme', active_color_scheme)

        window.run_command('show_panel', { 'panel': 'output.' + name })

    def write(self, text):
        self.view.run_command('append', { 'characters': text, 'scroll_to_end': True })

class ColorSchemeStyle():

    def __init__(self, view):
        self.view = view

        color_scheme = self.view.settings().get('color_scheme')
        color_scheme_resource = sublime.load_resource(color_scheme)
        color_scheme_plist = plistlib.readPlistFromBytes(bytes(color_scheme_resource, 'UTF-8'))
        self.color_scheme_plist_settings = color_scheme_plist['settings']

        self.default_style = dict()
        for color_scheme_plist_dict in self.color_scheme_plist_settings:
            if 'scope' not in color_scheme_plist_dict:
                self.default_style.update(color_scheme_plist_dict['settings'])

    def at_point(self, point):
        scope = self.view.scope_name(point).strip()

        last_selector_score = -1
        style = self.default_style.copy()
        for color_scheme_definition in self.color_scheme_plist_settings:
            if 'scope' in color_scheme_definition:
                score = sublime.score_selector(scope, color_scheme_definition['scope'])
                if score and score >= last_selector_score:
                    last_selector_score = score
                    style.update(color_scheme_definition['settings'])

        return style

def run_color_scheme_test(test, window, output):
    debug_message('running color scheme test: %s' % test)

    error = False
    failures = []
    assertion_count = 0

    test_view = TestView('color_scheme_unit', window)
    test_view.setUp()

    try:
        test_content = sublime.load_resource(test)

        color_test_params = COLOR_TEST_PARAMS_COMPILED_PATTERN.match(test_content)

        if not color_test_params:
            error = {
                'message': 'Invalid color scheme test: unable to find valid COLOR SCHEME TEST marker',
                'file': os.path.join(os.path.dirname(sublime.packages_path()), test),
                'row': 0,
                'col': 0
            }
            raise RuntimeError(error['message'])

        syntaxes = sublime.find_resources(color_test_params.group('syntax_name') + '.sublime-syntax')

        if len(syntaxes) > 1:
            error = {
                'message': 'Too many syntaxes found',
                'file': os.path.join(os.path.dirname(sublime.packages_path()), test),
                'row': 0,
                'col': 0
            }
            raise RuntimeError(error['message'])

        if len(syntaxes) is not 1:
            error = {
                'message': 'Syntaxes not found',
                'file': os.path.join(os.path.dirname(sublime.packages_path()), test),
                'row': 0,
                'col': 0
            }
            raise RuntimeError(error['message'])

        color_scheme = 'Packages/' + color_test_params.group('color_scheme')

        test_view.view.settings().set('color_scheme', color_scheme)
        test_view.view.assign_syntax(syntaxes[0])
        test_view.set_content(test_content)

        color_scheme_style = ColorSchemeStyle(test_view.view)

        consecutive_test_lines = 0
        for line_number, line in enumerate(test_content.splitlines()):
            assertion_params = COLOR_TEST_ASSERTION_COMPILED_PATTERN.match(line.lower().rstrip(' -->').rstrip(' */'))
            if not assertion_params:
                consecutive_test_lines = 0
                continue

            consecutive_test_lines += 1

            assertion = assertion_params.group(0)
            assertion_row = line_number - consecutive_test_lines
            assertion_begin = line.find('^')
            assertion_end = assertion_begin + len(assertion_params.group('repeat'))
            expected_styles = {}
            if assertion_params.group('fg') is not None:
                expected_styles['foreground'] = assertion_params.group('fg')
            if assertion_params.group('bg') is not None:
                expected_styles['background'] = assertion_params.group('bg')
            if assertion_params.group('fs') is not None:
                expected_styles['fontStyle'] = assertion_params.group('fs')

            for col in range(assertion_begin, assertion_end):
                assertion_count += 1
                assertion_point = test_view.view.text_point(assertion_row, col)
                actual_styles_at_point = color_scheme_style.at_point(assertion_point)

                actual_styles = {}
                for style in expected_styles:
                    if style in actual_styles_at_point:
                        if actual_styles_at_point[style]:
                            actual_styles[style] = actual_styles_at_point[style].lower()
                        else:
                            actual_styles[style] = actual_styles_at_point[style]
                    else:
                        actual_styles[style] = ''

                progress_character='.'
                if actual_styles != expected_styles:
                    progress_character='F'

                    failure_trace = {
                        'assertion': assertion,
                        'file': os.path.join(os.path.dirname(sublime.packages_path()), test),
                        'row': assertion_row + 2,
                        'col': col + 1,
                        'actual': actual_styles,
                        'expected': expected_styles,
                    }

                    debug_message('----- Assertion FAILED! -----')
                    debug_message('Trace: %s' % str(failure_trace))

                    failures.append(failure_trace)

                output.write(progress_character)
                if assertion_count % 80 == 0:
                    output.write("\n")

        output.write("\n")
    except Exception as e:
        test_view.tearDown()
        if not error:
            output.write(str(e))
            output.write("\n")

    test_view.tearDown()

    return {
        'error': error,
        'failures': failures,
        'assertions': assertion_count
    }

def is_valid_color_scheme_test_file_name(file_name):
    return bool(re.match('^color_scheme_test.*\.[a-zA-Z0-9]+$', os.path.basename(file_name)))

class RunColorSchemeTestCommand(sublime_plugin.TextCommand):

    def run(self, edit):
        file_name = self.view.file_name()
        if file_name:
            if is_valid_color_scheme_test_file_name(file_name):
                window = self.view.window()
                if window:
                    ColorSchemeUnit(window).run(file_name)

class RunColorSchemeTestsCommand(sublime_plugin.WindowCommand):

    def run(self):
        ColorSchemeUnit(self.window).run()

class ColorSchemeUnit():

    def __init__(self, window):
        self.window = window

    def run(self, file = None):
        sublime.set_timeout_async(lambda: self.run_async(file))

    def run_async(self, test_file):
        view = self.window.active_view()
        if not view:
            return

        file_name = view.file_name()
        if not file_name:
            return

        def normalise_resource_path(path):
            if sublime.platform() == 'windows':
                path = re.sub(r"/", r"\\", path)
            return path

        tests = []
        for resource in sublime.find_resources('color_scheme_test*'):
            package = resource.split('/')[1]
            package_path = os.path.join(sublime.packages_path(), package)
            if file_name.startswith(package_path):
                tests_package_name = package
                if test_file:
                    resource_file = os.path.join(os.path.dirname(sublime.packages_path()), normalise_resource_path(resource))
                    if test_file == resource_file:
                        tests.append(resource)
                else:
                    tests.append(resource)

        if not len(tests):
            return

        output = TestOutputPanel('color_scheme_unit', self.window)
        output.write("ColorSchemeUnit %s\n\n" % __version__)
        output.write("Runtime: %s build %s\n" % (sublime.platform(), sublime.version()))
        output.write("Package: %s\n" % tests_package_name)
        if test_file:
            output.write("File:    %s\n" % test_file)
        output.write("\n")

        errors = []
        failures = []
        total_assertions = 0

        start = timer()
        for test in tests:
            test_result = run_color_scheme_test(test, self.window, output)
            if test_result['error']:
                errors += [test_result['error']]
            failures += test_result['failures']
            total_assertions += test_result['assertions']
        elapsed = timer() - start
        output.write("\n")

        output.write("Time: %.2f secs\n" % (elapsed))
        output.write("\n")

        if len(errors) > 0:
            output.write("There were %s errors:\n" % len(errors))
            output.write("\n")
            for i, error in enumerate(errors, start=1):
                output.write("%d) %s\n" % (i, error['message']))
                output.write("%s:%d:%d\n" % (error['file'], error['row'], error['col']))
                output.write("\n")

        if len(failures) > 0:
            output.write("There were %s failures:\n\n" % len(failures))
            for i, failure in enumerate(failures, start=1):
                output.write("%d) %s\n" % (i, failure['assertion']))
                output.write("Failed asserting %s equals %s\n" % (str(failure['actual']), str(failure['expected'])))
                # TODO failure diff
                # output.write("--- Expected\n")
                # output.write("+++ Actual\n")
                # output.write("@@ @@\n")
                # output.write("{{diff}}\n\n")
                output.write("%s:%d:%d\n" % (failure['file'], failure['row'], failure['col']))
                output.write("\n")

        if len(errors) == 0 and len(failures) == 0:
            output.write("OK (%d tests, %d assertions)\n" % (len(tests), total_assertions))
        else:
            output.write("FAILURES!\n")
            output.write("Tests: %d, Assertions: %d" % (len(tests), total_assertions))
            if len(errors) > 0:
                output.write(", Errors: %d" % (len(errors)))
            output.write(", Failures: %d" % (len(failures)))
            output.write(".")
            output.write("\n")

if os.getenv('SUBLIME_COLOR_SCHEME_UNIT_DEBUG'):

    def copy(view, text):
        sublime.set_clipboard(text)
        view.hide_popup()
        sublime.status_message('Scope name copied to clipboard')

    class ShowScopeNameAndStylesCommand(sublime_plugin.TextCommand):

        def run(self, edit):
            scope = self.view.scope_name(self.view.sel()[-1].b)
            style = ColorSchemeStyle(self.view).at_point(self.view.sel()[-1].b)

            style_html = '
'
            if 'foreground' in style:
                style_html += "
foreground: {0}
".format(style['foreground'])
                del style['foreground']
            if 'background' in style:
                style_html += "
background: {0}
".format(style['background'])
                del style['background']
            if 'fontStyle' in style:
                style_html += "
fontStyle: {0}
".format(style['fontStyle'])
                del style['fontStyle']
            for x in sorted(style):
                style_html += "
{0}: {1}
".format(x, style[x])
            style_html += '
';

            html = """

%s

                    Copy
                    %s

            """ % (scope.replace(' ', '
'), scope.rstrip(), style_html)

            self.view.show_popup(html, max_width=512, on_navigate=lambda x: copy(self.view, x))

    class SetColorSchemeOnLoad(sublime_plugin.EventListener):

        def on_load_async(self, view):
            file_name = view.file_name()
            if file_name:
                if is_valid_color_scheme_test_file_name(file_name):
                    color_scheme_params = COLOR_TEST_PARAMS_COMPILED_PATTERN.match(view.substr(sublime.Region(0, view.size())))
                    if color_scheme_params:
                        view.settings().set('color_scheme', 'Packages/' + color_scheme_params.group('color_scheme'))
<commit_msg>Debug messages<commit_after>
__version__ = "0.13.1"
__version_info__ = (0, 12, 0)

import os
import re
import plistlib
from timeit import default_timer as timer

import sublime
import sublime_plugin

if os.getenv('SUBLIME_COLOR_SCHEME_UNIT_DEBUG'):
    def debug_message(message):
        print('DEBUG color_scheme_unit: %s' % str(message))
else:
    def debug_message(message):
        pass

COLOR_TEST_PARAMS_COMPILED_PATTERN = re.compile('^(?:(?:\<\?php )?(?://|#|\/\*|\<\!--)\s*)?COLOR SCHEME TEST "(?P<color_scheme>[^"]+)" "(?P<syntax_name>[^"]+)"(?:\s*(?:--\>|\?\>|\*\/))?')
COLOR_TEST_ASSERTION_COMPILED_PATTERN = re.compile('^(//|#|\/\*|\<\!--)\s*(?P<repeat>\^+)(?: fg=(?P<fg>[^ ]+)?)?(?: bg=(?P<bg>[^ ]+)?)?(?: fs=(?P<fs>[^=]*)?)?$')

class _color_scheme_unit_test_view_set_content(sublime_plugin.TextCommand):

    def run(self, edit, content):
        self.view.erase(edit, sublime.Region(0, self.view.size()))
        self.view.insert(edit, 0, content)

class TestView():

    def __init__(self, name, window):
        self.name = name + '_test_view'
        self.window = window

    def setUp(self):
        self.view = self.window.create_output_panel(self.name, unlisted=True)

    def tearDown(self):
        if self.view:
            self.view.close()

    def set_content(self, content):
        self.view.run_command('_color_scheme_unit_test_view_set_content', {
            'content': content
        })

    def get_content(self):
        return self.view.substr(sublime.Region(0, self.view.size()))

class TestOutputPanel():

    def __init__(self, name, window):
        self.view = window.create_output_panel(name)

        settings = self.view.settings()
        settings.set('result_file_regex', '^(.+):([0-9]+):([0-9]+)$')
        settings.set('word_wrap', False)
        settings.set('line_numbers', False)
        settings.set('gutter', False)
        settings.set('rulers', [])
        settings.set('scroll_past_end', False)

        self.view.assign_syntax('Packages/color_scheme_unit/test_output_panel.sublime-syntax')

        # Assign panel with color scheme of active view
        active_view = window.active_view()
        if active_view:
            active_color_scheme = active_view.settings().get('color_scheme')
            if active_color_scheme:
                settings.set('color_scheme', active_color_scheme)

        window.run_command('show_panel', { 'panel': 'output.' + name })

    def write(self, text):
        self.view.run_command('append', { 'characters': text, 'scroll_to_end': True })

class ColorSchemeStyle():

    def __init__(self, view):
        self.view = view

        color_scheme = self.view.settings().get('color_scheme')
        color_scheme_resource = sublime.load_resource(color_scheme)
        color_scheme_plist = plistlib.readPlistFromBytes(bytes(color_scheme_resource, 'UTF-8'))
        self.color_scheme_plist_settings = color_scheme_plist['settings']

        self.default_style = dict()
        for color_scheme_plist_dict in self.color_scheme_plist_settings:
            if 'scope' not in color_scheme_plist_dict:
                self.default_style.update(color_scheme_plist_dict['settings'])

    def at_point(self, point):
        scope = self.view.scope_name(point).strip()

        last_selector_score = -1
        style = self.default_style.copy()
        for color_scheme_definition in self.color_scheme_plist_settings:
            if 'scope' in color_scheme_definition:
                score = sublime.score_selector(scope, color_scheme_definition['scope'])
                if score and score >= last_selector_score:
                    last_selector_score = score
                    style.update(color_scheme_definition['settings'])

        return style

def run_color_scheme_test(test, window, output):
    debug_message('running test "%s"' % test)

    error = False
    failures = []
    assertion_count = 0

    test_view = TestView('color_scheme_unit', window)
    test_view.setUp()

    try:
        test_content = sublime.load_resource(test)

        color_test_params = COLOR_TEST_PARAMS_COMPILED_PATTERN.match(test_content)
        if not color_test_params:
            error = {
                'message': 'Invalid color scheme test: unable to find valid COLOR SCHEME TEST marker',
                'file': os.path.join(os.path.dirname(sublime.packages_path()), test),
                'row': 0,
                'col': 0
            }
            raise RuntimeError(error['message'])

        syntaxes = sublime.find_resources(color_test_params.group('syntax_name') + '.sublime-syntax')
        if len(syntaxes) > 1:
            error = {
                'message': 'Too many syntaxes found',
                'file': os.path.join(os.path.dirname(sublime.packages_path()), test),
                'row': 0,
                'col': 0
            }
            raise RuntimeError(error['message'])
        if len(syntaxes) is not 1:
            error = {
                'message': 'Syntaxes not found',
                'file': os.path.join(os.path.dirname(sublime.packages_path()), test),
                'row': 0,
                'col': 0
            }
            raise RuntimeError(error['message'])

        color_scheme = 'Packages/' + color_test_params.group('color_scheme')
        syntax = syntaxes[0]

        debug_message('color_scheme "%s"' % color_scheme)
        debug_message('syntax "%s"' % syntax)

        test_view.view.settings().set('color_scheme', color_scheme)
        test_view.view.assign_syntax(syntax)
        test_view.set_content(test_content)

        color_scheme_style = ColorSchemeStyle(test_view.view)

        consecutive_test_lines = 0
        for line_number, line in enumerate(test_content.splitlines()):
            assertion_params = COLOR_TEST_ASSERTION_COMPILED_PATTERN.match(line.lower().rstrip(' -->').rstrip(' */'))
            if not assertion_params:
                consecutive_test_lines = 0
                continue

            consecutive_test_lines += 1

            assertion = assertion_params.group(0)
            assertion_row = line_number - consecutive_test_lines
            assertion_begin = line.find('^')
            assertion_end = assertion_begin + len(assertion_params.group('repeat'))
            expected_styles = {}
            if assertion_params.group('fg') is not None:
                expected_styles['foreground'] = assertion_params.group('fg')
            if assertion_params.group('bg') is not None:
                expected_styles['background'] = assertion_params.group('bg')
            if assertion_params.group('fs') is not None:
                expected_styles['fontStyle'] = assertion_params.group('fs')

            for col in range(assertion_begin, assertion_end):
                assertion_count += 1
                assertion_point = test_view.view.text_point(assertion_row, col)
                actual_styles_at_point = color_scheme_style.at_point(assertion_point)

                actual_styles = {}
                for style in expected_styles:
                    if style in actual_styles_at_point:
                        if actual_styles_at_point[style]:
                            actual_styles[style] = actual_styles_at_point[style].lower()
                        else:
                            actual_styles[style] = actual_styles_at_point[style]
                    else:
                        actual_styles[style] = ''

                progress_character='.'
                if actual_styles != expected_styles:
                    progress_character='F'

                    failure_trace = {
                        'assertion': assertion,
                        'file': os.path.join(os.path.dirname(sublime.packages_path()), test),
                        'row': assertion_row + 2,
                        'col': col + 1,
                        'actual': actual_styles,
                        'expected': expected_styles,
                    }

                    debug_message('Failure trace: %s' % str(failure_trace))

                    failures.append(failure_trace)

                output.write(progress_character)
                if assertion_count % 80 == 0:
                    output.write("\n")

        output.write("\n")
    except Exception as e:
        test_view.tearDown()
        if not error:
            output.write(str(e))
            output.write("\n")

    test_view.tearDown()

    return {
        'error': error,
        'failures': failures,
        'assertions': assertion_count
    }

def is_valid_color_scheme_test_file_name(file_name):
    return bool(re.match('^color_scheme_test.*\.[a-zA-Z0-9]+$', os.path.basename(file_name)))

class RunColorSchemeTestCommand(sublime_plugin.TextCommand):

    def run(self, edit):
        file_name = self.view.file_name()
        if file_name:
            if is_valid_color_scheme_test_file_name(file_name):
                window = self.view.window()
                if window:
                    ColorSchemeUnit(window).run(file_name)

class RunColorSchemeTestsCommand(sublime_plugin.WindowCommand):

    def run(self):
        ColorSchemeUnit(self.window).run()

class ColorSchemeUnit():

    def __init__(self, window):
        self.window = window

    def run(self, file = None):
        sublime.set_timeout_async(lambda: self.run_async(file))

    def run_async(self, test_file):
        view = self.window.active_view()
        if not view:
            return

        file_name = view.file_name()
        if not file_name:
            return

        def normalise_resource_path(path):
            if sublime.platform() == 'windows':
                path = re.sub(r"/", r"\\", path)
            return path

        tests = []
        for resource in sublime.find_resources('color_scheme_test*'):
            package = resource.split('/')[1]
            package_path = os.path.join(sublime.packages_path(), package)
            if file_name.startswith(package_path):
                tests_package_name = package
                if test_file:
                    resource_file = os.path.join(os.path.dirname(sublime.packages_path()), normalise_resource_path(resource))
                    if test_file == resource_file:
                        tests.append(resource)
                else:
                    tests.append(resource)

        if not len(tests):
            return

        output = TestOutputPanel('color_scheme_unit', self.window)
        output.write("ColorSchemeUnit %s\n\n" % __version__)
        output.write("Runtime: %s build %s\n" % (sublime.platform(), sublime.version()))
        output.write("Package: %s\n" % tests_package_name)
        if test_file:
            output.write("File:    %s\n" % test_file)
        output.write("\n")

        errors = []
        failures = []
        total_assertions = 0

        start = timer()
        for test in tests:
            test_result = run_color_scheme_test(test, self.window, output)
            if test_result['error']:
                errors += [test_result['error']]
            failures += test_result['failures']
            total_assertions += test_result['assertions']
        elapsed = timer() - start
        output.write("\n")

        output.write("Time: %.2f secs\n" % (elapsed))
        output.write("\n")

        if len(errors) > 0:
            output.write("There were %s errors:\n" % len(errors))
            output.write("\n")
            for i, error in enumerate(errors, start=1):
                output.write("%d) %s\n" % (i, error['message']))
                output.write("%s:%d:%d\n" % (error['file'], error['row'], error['col']))
                output.write("\n")

        if len(failures) > 0:
            output.write("There were %s failures:\n\n" % len(failures))
            for i, failure in enumerate(failures, start=1):
                output.write("%d) %s\n" % (i, failure['assertion']))
                output.write("Failed asserting %s equals %s\n" % (str(failure['actual']), str(failure['expected'])))
                # TODO failure diff
                # output.write("--- Expected\n")
                # output.write("+++ Actual\n")
                # output.write("@@ @@\n")
                # output.write("{{diff}}\n\n")
                output.write("%s:%d:%d\n" % (failure['file'], failure['row'], failure['col']))
                output.write("\n")

        if len(errors) == 0 and len(failures) == 0:
            output.write("OK (%d tests, %d assertions)\n" % (len(tests), total_assertions))
        else:
            output.write("FAILURES!\n")
            output.write("Tests: %d, Assertions: %d" % (len(tests), total_assertions))
            if len(errors) > 0:
                output.write(", Errors: %d" % (len(errors)))
            output.write(", Failures: %d" % (len(failures)))
            output.write(".")
            output.write("\n")

if os.getenv('SUBLIME_COLOR_SCHEME_UNIT_DEBUG'):

    def copy(view, text):
        sublime.set_clipboard(text)
        view.hide_popup()
        sublime.status_message('Scope name copied to clipboard')

    class ShowScopeNameAndStylesCommand(sublime_plugin.TextCommand):

        def run(self, edit):
            scope = self.view.scope_name(self.view.sel()[-1].b)
            style = ColorSchemeStyle(self.view).at_point(self.view.sel()[-1].b)

            style_html = '
'
            if 'foreground' in style:
                style_html += "
foreground: {0}
".format(style['foreground'])
                del style['foreground']
            if 'background' in style:
                style_html += "
background: {0}
".format(style['background'])
                del style['background']
            if 'fontStyle' in style:
                style_html += "
fontStyle: {0}
".format(style['fontStyle'])
                del style['fontStyle']
            for x in sorted(style):
                style_html += "
{0}: {1}
".format(x, style[x])
            style_html += '
';

            html = """

%s

                    Copy
                    %s

            """ % (scope.replace(' ', '
'), scope.rstrip(), style_html)

            self.view.show_popup(html, max_width=512, on_navigate=lambda x: copy(self.view, x))

    class SetColorSchemeOnLoad(sublime_plugin.EventListener):

        def on_load_async(self, view):
            file_name = view.file_name()
            if file_name:
                if is_valid_color_scheme_test_file_name(file_name):
                    color_scheme_params = COLOR_TEST_PARAMS_COMPILED_PATTERN.match(view.substr(sublime.Region(0, view.size())))
                    if color_scheme_params:
                        view.settings().set('color_scheme', 'Packages/' + color_scheme_params.group('color_scheme'))
<|endoftext|>


<commit_before># file openpyxl/reader/workbook.py

# Copyright (c) 2010 openpyxl
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# @license: http://www.opensource.org/licenses/mit-license.php
# @author: Eric Gazoni

"""Read in global settings to be maintained by the workbook object."""

# package imports
from openpyxl.shared.xmltools import fromstring, QName
from openpyxl.shared.ooxml import NAMESPACES
from openpyxl.workbook import DocumentProperties
from openpyxl.shared.date_time import W3CDTF_to_datetime
from openpyxl.namedrange import NamedRange, split_named_range

# constants
BUGGY_NAMED_RANGES = ['NA()', '#REF!']
DISCARDED_RANGES = ['Excel_BuiltIn', 'Print_Area']

def read_properties_core(xml_source):
    """Read assorted file properties."""
    properties = DocumentProperties()
    root = fromstring(xml_source)
    creator_node = root.find(QName(NAMESPACES['dc'], 'creator').text)
    if creator_node is not None:
        properties.creator = creator_node.text
    else:
        properties.creator = ''
    last_modified_by_node = root.find(
            QName(NAMESPACES['cp'], 'lastModifiedBy').text)
    if last_modified_by_node is not None:
        properties.last_modified_by = last_modified_by_node.text
    else:
        properties.last_modified_by = ''
    properties.created = W3CDTF_to_datetime(
            root.find(QName(NAMESPACES['dcterms'], 'created').text).text)
    properties.modified = W3CDTF_to_datetime(
            root.find(QName(NAMESPACES['dcterms'], 'modified').text).text)
    return properties

def get_number_of_parts(xml_source):
    """Get a list of contents of the workbook."""
    parts_size = {}
    parts_names = []
    root = fromstring(xml_source)
    heading_pairs = root.find(QName('http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
            'HeadingPairs').text)
    vector = heading_pairs.find(QName(NAMESPACES['vt'], 'vector').text)
    children = vector.getchildren()
    for child_id in range(0, len(children), 2):
        part_name = children[child_id].find(QName(NAMESPACES['vt'],
                'lpstr').text).text
        if not part_name in parts_names:
            parts_names.append(part_name)
        part_size = int(children[child_id + 1].find(QName(
                NAMESPACES['vt'], 'i4').text).text)
        parts_size[part_name] = part_size
    return parts_size, parts_names

def read_sheets_titles(xml_source):
    """Read titles for all sheets."""
    root = fromstring(xml_source)
    titles_root = root.find(QName('http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
            'TitlesOfParts').text)
    vector = titles_root.find(QName(NAMESPACES['vt'], 'vector').text)
    parts, names = get_number_of_parts(xml_source)

    # we can't assume 'Worksheets' to be written in english,
    # but it's always the first item of the parts list (see bug #22)
    size = parts[names[0]]
    children = [c.text for c in vector.getchildren()]
    return children[:size]

def read_named_ranges(xml_source, workbook):
    """Read named ranges, excluding poorly defined ranges."""
    named_ranges = []
    root = fromstring(xml_source)
    names_root = root.find(QName('http://schemas.openxmlformats.org/spreadsheetml/2006/main',
            'definedNames').text)
    if names_root:

        for name_node in names_root.getchildren():
            range_name = name_node.get('name')

            if name_node.get("hidden", '0') == '1':
                continue

            good_range = True

            for discarded_range in DISCARDED_RANGES:
                if discarded_range in range_name:
                    good_range = False
                    break

            for bad_range in BUGGY_NAMED_RANGES:
                if bad_range in name_node.text:
                    good_range = False
                    break

            if good_range:
                worksheet_name, xlrange = split_named_range(name_node.text)
                worksheet = workbook.get_sheet_by_name(worksheet_name)

                # it can happen that a valid named range references
                # a missing worksheet, when Excel didn't properly maintain
                # the named range list
                #
                # we just ignore them here
                if worksheet:
                    named_range = NamedRange(range_name, worksheet, xlrange)
                    named_ranges.append(named_range)

    return named_ranges
<commit_msg>[reader] small fix to avoid a warning in 2.7 (fixes #34)<commit_after># file openpyxl/reader/workbook.py

# Copyright (c) 2010 openpyxl
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# @license: http://www.opensource.org/licenses/mit-license.php
# @author: Eric Gazoni

"""Read in global settings to be maintained by the workbook object."""

# package imports
from openpyxl.shared.xmltools import fromstring, QName
from openpyxl.shared.ooxml import NAMESPACES
from openpyxl.workbook import DocumentProperties
from openpyxl.shared.date_time import W3CDTF_to_datetime
from openpyxl.namedrange import NamedRange, split_named_range

# constants
BUGGY_NAMED_RANGES = ['NA()', '#REF!']
DISCARDED_RANGES = ['Excel_BuiltIn', 'Print_Area']

def read_properties_core(xml_source):
    """Read assorted file properties."""
    properties = DocumentProperties()
    root = fromstring(xml_source)
    creator_node = root.find(QName(NAMESPACES['dc'], 'creator').text)
    if creator_node is not None:
        properties.creator = creator_node.text
    else:
        properties.creator = ''
    last_modified_by_node = root.find(
            QName(NAMESPACES['cp'], 'lastModifiedBy').text)
    if last_modified_by_node is not None:
        properties.last_modified_by = last_modified_by_node.text
    else:
        properties.last_modified_by = ''
    properties.created = W3CDTF_to_datetime(
            root.find(QName(NAMESPACES['dcterms'], 'created').text).text)
    properties.modified = W3CDTF_to_datetime(
            root.find(QName(NAMESPACES['dcterms'], 'modified').text).text)
    return properties

def get_number_of_parts(xml_source):
    """Get a list of contents of the workbook."""
    parts_size = {}
    parts_names = []
    root = fromstring(xml_source)
    heading_pairs = root.find(QName('http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
            'HeadingPairs').text)
    vector = heading_pairs.find(QName(NAMESPACES['vt'], 'vector').text)
    children = vector.getchildren()
    for child_id in range(0, len(children), 2):
        part_name = children[child_id].find(QName(NAMESPACES['vt'],
                'lpstr').text).text
        if not part_name in parts_names:
            parts_names.append(part_name)
        part_size = int(children[child_id + 1].find(QName(
                NAMESPACES['vt'], 'i4').text).text)
        parts_size[part_name] = part_size
    return parts_size, parts_names

def read_sheets_titles(xml_source):
    """Read titles for all sheets."""
    root = fromstring(xml_source)
    titles_root = root.find(QName('http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
            'TitlesOfParts').text)
    vector = titles_root.find(QName(NAMESPACES['vt'], 'vector').text)
    parts, names = get_number_of_parts(xml_source)

    # we can't assume 'Worksheets' to be written in english,
    # but it's always the first item of the parts list (see bug #22)
    size = parts[names[0]]
    children = [c.text for c in vector.getchildren()]
    return children[:size]

def read_named_ranges(xml_source, workbook):
    """Read named ranges, excluding poorly defined ranges."""
    named_ranges = []
    root = fromstring(xml_source)
    names_root = root.find(QName('http://schemas.openxmlformats.org/spreadsheetml/2006/main',
            'definedNames').text)
    if names_root is not None:

        for name_node in names_root.getchildren():
            range_name = name_node.get('name')

            if name_node.get("hidden", '0') == '1':
                continue

            good_range = True

            for discarded_range in DISCARDED_RANGES:
                if discarded_range in range_name:
                    good_range = False
                    break

            for bad_range in BUGGY_NAMED_RANGES:
                if bad_range in name_node.text:
                    good_range = False
                    break

            if good_range:
                worksheet_name, xlrange = split_named_range(name_node.text)
                worksheet = workbook.get_sheet_by_name(worksheet_name)

                # it can happen that a valid named range references
                # a missing worksheet, when Excel didn't properly maintain
                # the named range list
                #
                # we just ignore them here
                if worksheet:
                    named_range = NamedRange(range_name, worksheet, xlrange)
                    named_ranges.append(named_range)

    return named_ranges
<|endoftext|>


<commit_before>from __future__ import division
import os
import time
import xbmc
import xbmcaddon
import xbmcgui
import resources.lib.helpers as helpers
import resources.lib.spotipyControl as spotipyControl
import resources.lib.spotipy.util as util
from threading import Thread

# get addon info and set globals
addon = xbmcaddon.Addon()
addonpath = addon.getAddonInfo('path')
addon_id = addon.getAddonInfo('id').decode("utf-8")
cachepath = xbmc.translatePath(u"special://profile/addon_data/%s/" % (addon_id,)).decode("utf-8")
username = addon.getSetting('username')
client_id = addon.getSetting('client_id')
client_secret = addon.getSetting('client_secret')

# Worker Thread that checks the server for changes
windowopen = True   # global used to tell the worker thread the status of the window

def updateWindow(name, gui):
    time.sleep(0.5)
    track_old = {'item': '', 'is_playing': False, 'shuffle_state': 'off'}
    while windowopen and (not xbmc.abortRequested):
        track = sp.get_player()
        if track['item'] != track_old['item'] or track['is_playing'] != track_old['is_playing'] or track['shuffle_state'] != track_old['shuffle_state'] or track['repeat_state'] != track_old['repeat_state']:
            # insert update functions here
            gui.onUpdate(track)
            track_old = track
        gui.setProgress(track['progress_ms'], track['item']['duration_ms'])
        time.sleep(1)

# Define the GUI
class Main(xbmcgui.WindowXML):

    def __init__(self, *args, **kwargs):
        pass

    def onInit(self):
        # define controls
        self.win = xbmcgui.Window(10001)
        self.background = self.getControl(1001)
        self.cover = self.getControl(1002)
        self.play = self.getControl(1101)
        self.pause = self.getControl(1102)
        self.progress = self.getControl(3001)
        self.shuffle = self.getControl(1008)
        # self.repeat = self.getControl(1007)
        self.inLib = self.getControl(1009)
        self.next = self.getControl(1005)
        self.previous = self.getControl(1006)
        self.repeat_all = self.getControl(1201)
        self.repeat_one = self.getControl(1202)
        self.repeat_off = self.getControl(1203)
        self.volume_button = self.getControl(1011)

    def onAction(self, action):
        if action.getId() == 79:
            self.togglePlayState()
        elif action.getId() == 92:
            self.close()

    def onClick(self, controlID):
        if controlID == 1101:
            sp.play()
        if controlID == 1102:
            sp.pause()
        if controlID == 1005:
            sp.next()
        if controlID == 1006:
            sp.previous()
        if controlID == 1008:
            if self.shuffle.isSelected():
                sp.shuffle(True)
            else:
                sp.shuffle(False)
        if controlID == 1201:
            sp.repeat('track')
        if controlID == 1202:
            sp.repeat('off')
        if controlID == 1203:
            sp.repeat('context')
        if controlID == 1009:
            track = sp.get_player()
            if sp.current_user_saved_tracks_contains([track['item']['uri']])[0] is False:
                sp.current_user_saved_tracks_add([track['item']['uri']])
            elif sp.current_user_saved_tracks_contains([track['item']['uri']])[0] is True:
                sp.current_user_saved_tracks_delete([track['item']['uri']])
        if controlID == 1011:
            sp.put_player(helpers.chooseDevice(sp.get_devices()['devices']))

    def onUpdate(self, track):
        coverurl = track['item']['album']['images'][0]['url']
        self.cover.setImage(coverurl, False)
        self.background.setImage(helpers.blurCover(coverurl, cachepath), False)
        self.win.setProperty('title', track['item']['name'])
        self.win.setProperty('artist', helpers.getArtists(track))
        self.win.setProperty('album', track['item']['album']['name'])
        self.choseRepeat(track['repeat_state'])
        self.togglePlayPause(track['is_playing'])
        self.shuffle.setSelected(track['shuffle_state'])
        self.win.setProperty('device', track['device']['name'])

    def setProgress(self, progress_ms, duration_ms):
        progress = (100 * progress_ms) / duration_ms
        self.progress.setPercent(100 - progress)
        self.win.setProperty('progressTime', helpers.secondsToString(progress_ms // 1000))
        self.win.setProperty('remainingTime', '- ' + helpers.secondsToString((duration_ms - progress_ms) // 1000))

    def togglePlayPause(self, is_playing):
        self.pause.setVisible(is_playing)
        if is_playing:
            self.next.controlLeft(self.pause)
            self.previous.controlRight(self.pause)
        else:
            self.setFocus(self.play)
            self.next.controlLeft(self.play)
            self.previous.controlRight(self.play)

    def choseRepeat(self, repeat_state):
        if repeat_state == 'context':
            self.repeat_all.setVisible(True)
            self.repeat_one.setVisible(False)
            self.repeat_off.setVisible(False)
            self.volume_button.controlLeft(self.repeat_all)
            self.next.controlRight(self.repeat_all)

        if repeat_state == 'track':
            self.repeat_all.setVisible(False)
            self.repeat_one.setVisible(True)
            self.repeat_off.setVisible(False)
            self.volume_button.controlLeft(self.repeat_one)
            self.next.controlRight(self.repeat_one)

        if repeat_state == 'off':
            self.repeat_all.setVisible(False)
            self.repeat_one.setVisible(False)
            self.repeat_off.setVisible(True)
            self.volume_button.controlLeft(self.repeat_off)
            self.next.controlRight(self.repeat_off)

if __name__ == '__main__':
    # make cache directory
    if not os.path.exists(cachepath):
        xbmc.log('Creating directory for cache:' + cachepath)
        os.makedirs(cachepath)

    # get token
    token = util.prompt_for_user_token(username, cachepath, client_id=client_id, client_secret=client_secret)
    if token:
        sp = spotipyControl.SpotipyControl(auth=token)

        # create the GUI
        gui = Main("connectcontrol-main.xml", addonpath)

        # create and start a separate thread for the looping process that updates the window
        t1 = Thread(target=updateWindow, args=("thread 1", gui))
        t1.setDaemon(True)

        # run everything
        t1.start()
        gui.doModal()

        # delete the gui and the worker thread
        windowopen = False
        del gui
        del t1
    else:
        xbmc.log('Can\'t get token for ' + username)
<commit_msg>Bugfix - Play - Remote<commit_after>from __future__ import division
import os
import time
import xbmc
import xbmcaddon
import xbmcgui
import resources.lib.helpers as helpers
import resources.lib.spotipyControl as spotipyControl
import resources.lib.spotipy.util as util
from threading import Thread

# get addon info and set globals
addon = xbmcaddon.Addon()
addonpath = addon.getAddonInfo('path')
addon_id = addon.getAddonInfo('id').decode("utf-8")
cachepath = xbmc.translatePath(u"special://profile/addon_data/%s/" % (addon_id,)).decode("utf-8")
username = addon.getSetting('username')
client_id = addon.getSetting('client_id')
client_secret = addon.getSetting('client_secret')

# Worker Thread that checks the server for changes
windowopen = True   # global used to tell the worker thread the status of the window

def updateWindow(name, gui):
    time.sleep(0.5)
    track_old = {'item': '', 'is_playing': False, 'shuffle_state': 'off'}
    while windowopen and (not xbmc.abortRequested):
        track = sp.get_player()
        if track['item'] != track_old['item'] or track['is_playing'] != track_old['is_playing'] or track['shuffle_state'] != track_old['shuffle_state'] or track['repeat_state'] != track_old['repeat_state']:
            # insert update functions here
            gui.onUpdate(track)
            track_old = track
        gui.setProgress(track['progress_ms'], track['item']['duration_ms'])
        time.sleep(1)

# Define the GUI
class Main(xbmcgui.WindowXML):

    def __init__(self, *args, **kwargs):
        pass

    def onInit(self):
        # define controls
        self.win = xbmcgui.Window(10001)
        self.background = self.getControl(1001)
        self.cover = self.getControl(1002)
        self.play = self.getControl(1101)
        self.pause = self.getControl(1102)
        self.progress = self.getControl(3001)
        self.shuffle = self.getControl(1008)
        # self.repeat = self.getControl(1007)
        self.inLib = self.getControl(1009)
        self.next = self.getControl(1005)
        self.previous = self.getControl(1006)
        self.repeat_all = self.getControl(1201)
        self.repeat_one = self.getControl(1202)
        self.repeat_off = self.getControl(1203)
        self.volume_button = self.getControl(1011)

    def onAction(self, action):
        if action.getId() == 79:
            self.togglePlayPause()
        elif action.getId() == 92:
            self.close()

    def onClick(self, controlID):
        if controlID == 1101:
            sp.play()
        if controlID == 1102:
            sp.pause()
        if controlID == 1005:
            sp.next()
        if controlID == 1006:
            sp.previous()
        if controlID == 1008:
            if self.shuffle.isSelected():
                sp.shuffle(True)
            else:
                sp.shuffle(False)
        if controlID == 1201:
            sp.repeat('track')
        if controlID == 1202:
            sp.repeat('off')
        if controlID == 1203:
            sp.repeat('context')
        if controlID == 1009:
            track = sp.get_player()
            if sp.current_user_saved_tracks_contains([track['item']['uri']])[0] is False:
                sp.current_user_saved_tracks_add([track['item']['uri']])
            elif sp.current_user_saved_tracks_contains([track['item']['uri']])[0] is True:
                sp.current_user_saved_tracks_delete([track['item']['uri']])
        if controlID == 1011:
            sp.put_player(helpers.chooseDevice(sp.get_devices()['devices']))

    def onUpdate(self, track):
        coverurl = track['item']['album']['images'][0]['url']
        self.cover.setImage(coverurl, False)
        self.background.setImage(helpers.blurCover(coverurl, cachepath), False)
        self.win.setProperty('title', track['item']['name'])
        self.win.setProperty('artist', helpers.getArtists(track))
        self.win.setProperty('album', track['item']['album']['name'])
        self.choseRepeat(track['repeat_state'])
        self.togglePlayPause(track['is_playing'])
        self.shuffle.setSelected(track['shuffle_state'])
        self.win.setProperty('device', track['device']['name'])

    def setProgress(self, progress_ms, duration_ms):
        progress = (100 * progress_ms) / duration_ms
        self.progress.setPercent(100 - progress)
        self.win.setProperty('progressTime', helpers.secondsToString(progress_ms // 1000))
        self.win.setProperty('remainingTime', '- ' + helpers.secondsToString((duration_ms - progress_ms) // 1000))

    def togglePlayPause(self, is_playing):
        self.pause.setVisible(is_playing)
        if is_playing:
            self.next.controlLeft(self.pause)
            self.previous.controlRight(self.pause)
        else:
            self.setFocus(self.play)
            self.next.controlLeft(self.play)
            self.previous.controlRight(self.play)

    def choseRepeat(self, repeat_state):
        if repeat_state == 'context':
            self.repeat_all.setVisible(True)
            self.repeat_one.setVisible(False)
            self.repeat_off.setVisible(False)
            self.volume_button.controlLeft(self.repeat_all)
            self.next.controlRight(self.repeat_all)

        if repeat_state == 'track':
            self.repeat_all.setVisible(False)
            self.repeat_one.setVisible(True)
            self.repeat_off.setVisible(False)
            self.volume_button.controlLeft(self.repeat_one)
            self.next.controlRight(self.repeat_one)

        if repeat_state == 'off':
            self.repeat_all.setVisible(False)
            self.repeat_one.setVisible(False)
            self.repeat_off.setVisible(True)
            self.volume_button.controlLeft(self.repeat_off)
            self.next.controlRight(self.repeat_off)

if __name__ == '__main__':
    # make cache directory
    if not os.path.exists(cachepath):
        xbmc.log('Creating directory for cache:' + cachepath)
        os.makedirs(cachepath)

    # get token
    token = util.prompt_for_user_token(username, cachepath, client_id=client_id, client_secret=client_secret)
    if token:
        sp = spotipyControl.SpotipyControl(auth=token)

        # create the GUI
        gui = Main("connectcontrol-main.xml", addonpath)

        # create and start a separate thread for the looping process that updates the window
        t1 = Thread(target=updateWindow, args=("thread 1", gui))
        t1.setDaemon(True)

        # run everything
        t1.start()
        gui.doModal()

        # delete the gui and the worker thread
        windowopen = False
        del gui
        del t1
    else:
        xbmc.log('Can\'t get token for ' + username)
<|endoftext|>


<commit_before>###
# Copyright (c) 2013, Richard Esplin
# Based on ChannelLogger by Jeremiah Fincher (c) 2005 and James McCoy (c) 2010
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
'''
Channel Logger that produces HTML files.
'''

import html
import os
import re
import shutil
import time
from io import StringIO

import supybot.conf as conf
import supybot.world as world
import supybot.ircdb as ircdb
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.registry as registry
import supybot.callbacks as callbacks
from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('HtmlLogger')

# This regex doesn't match every URL, but it is simple and gets most.
url_regex = re.compile("\s*([fhtps]{3,5}://\S+)\s*")

file_suffix = "html"
row_class = "style-row"
notice_class = "style-notice"
timestamp_class = "style-tz"
nick_class = "style-nick"
message_class = "style-msg"

class FakeLog(object):
    def flush(self):
        return
    def close(self):
        return
    def write(self, s):
        return

class HtmlLogger(callbacks.Plugin):
    noIgnore = True
    def __init__(self, irc):
        self.__parent = super(HtmlLogger, self)
        self.__parent.__init__(irc)
        self.lastMsgs = {}
        self.lastStates = {}
        self.logs = {}
        self.flusher = self.flush
        world.flushers.append(self.flusher)

    def die(self):
        self.log.debug('Logging is dying.')
        for log in self._logs():
            self.endLog(log)
        world.flushers = [x for x in world.flushers if x is not self.flusher]

    def __call__(self, irc, msg):
        try:
            # I don't know why I put this in, but it doesn't work, because it
            # doesn't call doNick or doQuit.
            # if msg.args and irc.isChannel(msg.args[0]):
            self.__parent.__call__(irc, msg)
            if irc in self.lastMsgs:
                if irc not in self.lastStates:
                    self.lastStates[irc] = irc.state.copy()
                self.lastStates[irc].addMsg(irc, self.lastMsgs[irc])
        finally:
            # We must make sure this always gets updated.
            self.lastMsgs[irc] = msg

    def reset(self):
        self.log.debug('Reset all logs.')
        for log in self._logs():
            self.endLog(log)
        self.logs.clear()
        self.lastMsgs.clear()
        self.lastStates.clear()

    def _logs(self):
        for logs in self.logs.values():
            for log in logs.values():
                yield log

    def startLog(self, logPath):
        self.log.info('Starting new log file: %s.' % logPath)
        header_template = self.registryValue('headerfile')
        if header_template == '':
            header_template = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                           "header.html")
        self.log.debug('Using header template from %s.' % header_template)
        shutil.copyfile(header_template, logPath)

    def getFooter(self):
        ''' Reads the footer, and returns it as a string for appending to the
            log file.
        '''
        footer_template = self.registryValue('footerfile')
        if footer_template == '':
            footer_template = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                           "footer.html")
        self.log.debug('Using footer template from %s.' % footer_template)
        footerString = ''
        with open(footer_template, encoding='utf-8', mode='r') as footerFile:
            footerString = footerFile.read()
        return footerString

    def endLog(self, log):
        self.log.debug('Closing log file.')
        footerString = self.getFooter()
        log.write(footerString)
        log.close()

    def flush(self):
        self.checkLogNames()
        for log in self._logs():
            try:
                log.flush()
            except ValueError as e:
                if e.args[0] != 'I/O operation on a closed file':
                    self.log.exception('Odd exception:')

    def logNameTimestamp(self, channel):
        format = self.registryValue('filenameTimestamp', channel)
        return time.strftime(format, time.gmtime())

    def getLogName(self, channel):
        if self.registryValue('rotateLogs', channel):
            return '%s.%s.%s' % (channel, self.logNameTimestamp(channel),
                                 file_suffix)
        else:
            return '%s.%s' % (channel, file_suffix)

    def getLogDir(self, irc, channel):
        logDir = conf.supybot.directories.log.dirize(self.name())
        if self.registryValue('directories'):
            if self.registryValue('directories.network'):
                logDir = os.path.join(logDir,  irc.network)
            if self.registryValue('directories.channel'):
                logDir = os.path.join(logDir, channel)
            if self.registryValue('directories.timestamp'):
                format = self.registryValue('directories.timestamp.format')
                timeDir =time.strftime(format, time.gmtime())
                logDir = os.path.join(logDir, timeDir)
        if not os.path.exists(logDir):
            os.makedirs(logDir)
        return logDir

    def checkLogNames(self):
        for (irc, logs) in list(self.logs.items()):
            for (channel, log) in list(logs.items()):
                if self.registryValue('rotateLogs', channel):
                    name = self.getLogName(channel)
                    if name != os.path.basename(log.name):
                        self.log.debug('Timestamp change. Start new log.')
                        self.endLog(log)
                        del logs[channel]
                        number2keep = self.registryValue('deleteOldLogs', channel)
                        if number2keep > 0:
                            self.deleteOldLogs(irc, channel, number2keep)

    def deleteOldLogs(self, irc, channel, number2keep):
        logDir = self.getLogDir(irc, channel)
        logFiles = [f for f in os.listdir(logDir)
                            if os.path.isfile(os.path.join(logDir, f))
                               and f.endswith("."+file_suffix)]
        logFiles.sort(reverse=True)
        need2delete = logFiles[number2keep:]
        if len(need2delete) > 0:
            self.log.info('Cleaning logs in "%s".', logDir)
            self.log.info('Will keep %s logfiles.', number2keep)
            for f in need2delete:
                self.log.info('Deleting old logfile "%s."', f)
                os.remove(os.path.join(logDir, f))

    def getLog(self, irc, channel):
        self.checkLogNames()
        try:
            logs = self.logs[irc]
        except KeyError:
            logs = ircutils.IrcDict()
            self.logs[irc] = logs
        if channel in logs:
            return logs[channel]
        else:
            try:
                name = self.getLogName(channel)
                logDir = self.getLogDir(irc, channel)
                logPath = os.path.join(logDir, name)
                if not os.path.isfile(logPath):
                    self.startLog(logPath)
                else: # Remove the footer if it is there
                    # This will not work with huge log files
                    with open(logPath, encoding='utf-8', mode='r') as logFile:
                        logFileString = logFile.read()
                    footerString = self.getFooter()
                    if logFileString.endswith(footerString):
                        with open(logPath, encoding='utf-8', mode='w') as logFile:
                            logFile.write(logFileString[:-len(footerString)])
                log = open(logPath, encoding='utf-8', mode='a')
                logs[channel] = log
                return log
            except IOError:
                self.log.exception('Error opening log:')
                return FakeLog()

    def timestamp(self, log):
        format = conf.supybot.log.timestampFormat()
        if format:
            log.write(time.strftime(format, time.gmtime()))
            log.write('  ')

    def normalizeChannel(self, irc, channel):
        return ircutils.toLower(channel)

    def linkify(self, message):
        '''Enclose all URLs in the message with an href.'''
        return url_regex.sub(r'\1', message)

    def doLog(self, irc, channel, notice, nick, s, *args):
        ''' notice: Boolean. True if message should be styled as a notice. '''
        if not self.registryValue('enable', channel):
            return
        s = format(s, *args)
        channel = self.normalizeChannel(irc, channel)
        log = self.getLog(irc, channel)
        row_classes = row_class
        if notice:
            row_classes = row_class + " " + notice_class
        log.write('
' % row_classes)
        if self.registryValue('timestamp', channel):
            log.write('' % timestamp_class)
            self.timestamp(log)
            log.write('')
        if nick != None:
            log.write('' % nick_class)
            log.write(html.escape("<%s> " %nick))
            log.write('')
        if self.registryValue('stripFormatting', channel):
            s = ircutils.stripFormatting(s)
        log.write('' % message_class)
        log.write(self.linkify(html.escape(s)))
        log.write('')
        log.write('
\n')
        if self.registryValue('flushImmediately'):
            log.flush()

    def doPrivmsg(self, irc, msg):
        (recipients, text) = msg.args
        for channel in recipients.split(','):
            if irc.isChannel(channel):
                noLogPrefix = self.registryValue('noLogPrefix', channel)
                cap = ircdb.makeChannelCapability(channel, 'logChannelMessages')
                try:
                    logChannelMessages = ircdb.checkCapability(msg.prefix, cap,
                        ignoreOwner=True)
                except KeyError:
                    logChannelMessages = True
                nick = msg.nick or irc.nick
                if msg.tagged('HtmlLogger__relayed'):
                    (nick, text) = text.split(' ', 1)
                    nick = nick[1:-1]
                    msg.args = (recipients, text)
                if (noLogPrefix and text.startswith(noLogPrefix)) or \
                        not logChannelMessages:
                    text = '-= THIS MESSAGE NOT LOGGED =-'
                if ircmsgs.isAction(msg):
                    self.doLog(irc, channel, False, None,
                               '* %s %s', nick, ircmsgs.unAction(msg))
                else:
                    self.doLog(irc, channel, False, nick, '%s', text)

    def doNotice(self, irc, msg):
        (recipients, text) = msg.args
        for channel in recipients.split(','):
            if irc.isChannel(channel):
                self.doLog(irc, channel, True, None, '-%s- %s', msg.nick, text)

    def doNick(self, irc, msg):
        oldNick = msg.nick
        newNick = msg.args[0]
        for (channel, c) in irc.state.channels.items():
            if newNick in c.users:
                self.doLog(irc, channel, True, None,
                           '*** %s is now known as %s', oldNick, newNick)
    def doJoin(self, irc, msg):
        for channel in msg.args[0].split(','):
            if(self.registryValue('showJoinParts', channel)):
                self.doLog(irc, channel, True, None,
                           '*** %s <%s> has joined %s',
                           msg.nick, msg.prefix, channel)

    def doKick(self, irc, msg):
        if len(msg.args) == 3:
            (channel, target, kickmsg) = msg.args
        else:
            (channel, target) = msg.args
            kickmsg = ''
        if kickmsg:
            self.doLog(irc, channel, True, None,
                       '*** %s was kicked by %s (%s)',
                       target, msg.nick, kickmsg)
        else:
            self.doLog(irc, channel, True, None,
                       '*** %s was kicked by %s', target, msg.nick)

    def doPart(self, irc, msg):
        if len(msg.args) > 1:
            reason = " (%s)" % msg.args[1]
        else:
            reason = ""
        for channel in msg.args[0].split(','):
            if(self.registryValue('showJoinParts', channel)):
                self.doLog(irc, channel, True, None,
                           '*** %s <%s> has left %s%s',
                           msg.nick, msg.prefix, channel, reason)

    def doMode(self, irc, msg):
        channel = msg.args[0]
        if irc.isChannel(channel) and msg.args[1:]:
            self.doLog(irc, channel, True, None,
                       '*** %s sets mode: %s %s',
                       msg.nick or msg.prefix, msg.args[1],
                        ' '.join(msg.args[2:]))

    def doTopic(self, irc, msg):
        if len(msg.args) == 1:
            return # It's an empty TOPIC just to get the current topic.
        channel = msg.args[0]
        self.doLog(irc, channel, True, None,
                   '*** %s changes topic to "%s"', msg.nick, msg.args[1])

    def doQuit(self, irc, msg):
        if len(msg.args) == 1:
            reason = " (%s)" % msg.args[0]
        else:
            reason = ""
        if not isinstance(irc, irclib.Irc):
            irc = irc.getRealIrc()
        for (channel, chan) in self.lastStates[irc].channels.items():
            if(self.registryValue('showJoinParts', channel)):
                if msg.nick in chan.users:
                    self.doLog(irc, channel, True, None,
                               '*** %s <%s> has quit IRC%s',
                               msg.nick, msg.prefix, reason)

    def outFilter(self, irc, msg):
        # Gotta catch my own messages *somehow* :)
        # Let's try this little trick...
        if msg.command in ('PRIVMSG', 'NOTICE'):
            # Other messages should be sent back to us.
            m = ircmsgs.IrcMsg(msg=msg, prefix=irc.prefix)
            if msg.tagged('relayedMsg'):
                m.tag('HtmlLogger__relayed')
            self(irc, m)
        return msg

Class = HtmlLogger
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
<commit_msg>Fix Python 2 compatibility.<commit_after>###
# Copyright (c) 2013, Richard Esplin
# Based on ChannelLogger by Jeremiah Fincher (c) 2005 and James McCoy (c) 2010
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
'''
Channel Logger that produces HTML files.
'''

import os
import re
import sys
import shutil
import time

if sys.version_info[0] >= 3:
    from io import StringIO
    from html import escape as html_escape
    bin_mode = ''
else:
    from cStringIO import StringIO
    from xml.sax.saxutils import escape as html_escape
    from io import open
    bin_mode = 'b'

import supybot.conf as conf
import supybot.world as world
import supybot.ircdb as ircdb
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.registry as registry
import supybot.callbacks as callbacks
from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('HtmlLogger')

# This regex doesn't match every URL, but it is simple and gets most.
url_regex = re.compile("\s*([fhtps]{3,5}://\S+)\s*")

file_suffix = "html"
row_class = "style-row"
notice_class = "style-notice"
timestamp_class = "style-tz"
nick_class = "style-nick"
message_class = "style-msg"

class FakeLog(object):
    def flush(self):
        return
    def close(self):
        return
    def write(self, s):
        return

class HtmlLogger(callbacks.Plugin):
    noIgnore = True
    def __init__(self, irc):
        self.__parent = super(HtmlLogger, self)
        self.__parent.__init__(irc)
        self.lastMsgs = {}
        self.lastStates = {}
        self.logs = {}
        self.flusher = self.flush
        world.flushers.append(self.flusher)

    def die(self):
        self.log.debug('Logging is dying.')
        for log in self._logs():
            self.endLog(log)
        world.flushers = [x for x in world.flushers if x is not self.flusher]

    def __call__(self, irc, msg):
        try:
            # I don't know why I put this in, but it doesn't work, because it
            # doesn't call doNick or doQuit.
            # if msg.args and irc.isChannel(msg.args[0]):
            self.__parent.__call__(irc, msg)
            if irc in self.lastMsgs:
                if irc not in self.lastStates:
                    self.lastStates[irc] = irc.state.copy()
                self.lastStates[irc].addMsg(irc, self.lastMsgs[irc])
        finally:
            # We must make sure this always gets updated.
            self.lastMsgs[irc] = msg

    def reset(self):
        self.log.debug('Reset all logs.')
        for log in self._logs():
            self.endLog(log)
        self.logs.clear()
        self.lastMsgs.clear()
        self.lastStates.clear()

    def _logs(self):
        for logs in self.logs.values():
            for log in logs.values():
                yield log

    def startLog(self, logPath):
        self.log.info('Starting new log file: %s.' % logPath)
        header_template = self.registryValue('headerfile')
        if header_template == '':
            header_template = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                           "header.html")
        self.log.debug('Using header template from %s.' % header_template)
        shutil.copyfile(header_template, logPath)

    def getFooter(self):
        ''' Reads the footer, and returns it as a string for appending to the
            log file.
        '''
        footer_template = self.registryValue('footerfile')
        if footer_template == '':
            footer_template = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                           "footer.html")
        self.log.debug('Using footer template from %s.' % footer_template)
        footerString = ''
        with open(footer_template, mode='r'+bin_mode) as footerFile:
            footerString = footerFile.read()
        return footerString

    def endLog(self, log):
        self.log.debug('Closing log file.')
        footerString = self.getFooter()
        log.write(footerString)
        log.close()

    def flush(self):
        self.checkLogNames()
        for log in self._logs():
            try:
                log.flush()
            except ValueError as e:
                if e.args[0] != 'I/O operation on a closed file':
                    self.log.exception('Odd exception:')

    def logNameTimestamp(self, channel):
        format = self.registryValue('filenameTimestamp', channel)
        return time.strftime(format, time.gmtime())

    def getLogName(self, channel):
        if self.registryValue('rotateLogs', channel):
            return '%s.%s.%s' % (channel, self.logNameTimestamp(channel),
                                 file_suffix)
        else:
            return '%s.%s' % (channel, file_suffix)

    def getLogDir(self, irc, channel):
        logDir = conf.supybot.directories.log.dirize(self.name())
        if self.registryValue('directories'):
            if self.registryValue('directories.network'):
                logDir = os.path.join(logDir,  irc.network)
            if self.registryValue('directories.channel'):
                logDir = os.path.join(logDir, channel)
            if self.registryValue('directories.timestamp'):
                format = self.registryValue('directories.timestamp.format')
                timeDir =time.strftime(format, time.gmtime())
                logDir = os.path.join(logDir, timeDir)
        if not os.path.exists(logDir):
            os.makedirs(logDir)
        return logDir

    def checkLogNames(self):
        for (irc, logs) in list(self.logs.items()):
            for (channel, log) in list(logs.items()):
                if self.registryValue('rotateLogs', channel):
                    name = self.getLogName(channel)
                    if name != os.path.basename(log.name):
                        self.log.debug('Timestamp change. Start new log.')
                        self.endLog(log)
                        del logs[channel]
                        number2keep = self.registryValue('deleteOldLogs', channel)
                        if number2keep > 0:
                            self.deleteOldLogs(irc, channel, number2keep)

    def deleteOldLogs(self, irc, channel, number2keep):
        logDir = self.getLogDir(irc, channel)
        logFiles = [f for f in os.listdir(logDir)
                            if os.path.isfile(os.path.join(logDir, f))
                               and f.endswith("."+file_suffix)]
        logFiles.sort(reverse=True)
        need2delete = logFiles[number2keep:]
        if len(need2delete) > 0:
            self.log.info('Cleaning logs in "%s".', logDir)
            self.log.info('Will keep %s logfiles.', number2keep)
            for f in need2delete:
                self.log.info('Deleting old logfile "%s."', f)
                os.remove(os.path.join(logDir, f))

    def getLog(self, irc, channel):
        self.checkLogNames()
        try:
            logs = self.logs[irc]
        except KeyError:
            logs = ircutils.IrcDict()
            self.logs[irc] = logs
        if channel in logs:
            return logs[channel]
        else:
            try:
                name = self.getLogName(channel)
                logDir = self.getLogDir(irc, channel)
                logPath = os.path.join(logDir, name)
                if not os.path.isfile(logPath):
                    self.startLog(logPath)
                else: # Remove the footer if it is there
                    # This will not work with huge log files
                    with open(logPath, mode='r'+bin_mode) as logFile:
                        logFileString = logFile.read()
                    footerString = self.getFooter()
                    if logFileString.endswith(footerString):
                        with open(logPath, mode='w'+bin_mode) as logFile:
                            logFile.write(logFileString[:-len(footerString)])
                log = open(logPath, mode='a'+bin_mode)
                logs[channel] = log
                return log
            except IOError:
                self.log.exception('Error opening log:')
                return FakeLog()

    def timestamp(self, log):
        format = conf.supybot.log.timestampFormat()
        if format:
            log.write(time.strftime(format, time.gmtime()))
            log.write('  ')

    def normalizeChannel(self, irc, channel):
        return ircutils.toLower(channel)

    def linkify(self, message):
        '''Enclose all URLs in the message with an href.'''
        return url_regex.sub(r'\1', message)

    def doLog(self, irc, channel, notice, nick, s, *args):
        ''' notice: Boolean. True if message should be styled as a notice. '''
        if not self.registryValue('enable', channel):
            return
        s = format(s, *args)
        channel = self.normalizeChannel(irc, channel)
        log = self.getLog(irc, channel)
        row_classes = row_class
        if notice:
            row_classes = row_class + " " + notice_class
        log.write('
' % row_classes)
        if self.registryValue('timestamp', channel):
            log.write('' % timestamp_class)
            self.timestamp(log)
            log.write('')
        if nick != None:
            log.write('' % nick_class)
            log.write(html_escape("<%s> " %nick))
            log.write('')
        if self.registryValue('stripFormatting', channel):
            s = ircutils.stripFormatting(s)
        log.write('' % message_class)
        log.write(self.linkify(html_escape(s)))
        log.write('')
        log.write('
\n')
        if self.registryValue('flushImmediately'):
            log.flush()

    def doPrivmsg(self, irc, msg):
        (recipients, text) = msg.args
        for channel in recipients.split(','):
            if irc.isChannel(channel):
                noLogPrefix = self.registryValue('noLogPrefix', channel)
                cap = ircdb.makeChannelCapability(channel, 'logChannelMessages')
                try:
                    logChannelMessages = ircdb.checkCapability(msg.prefix, cap,
                        ignoreOwner=True)
                except KeyError:
                    logChannelMessages = True
                nick = msg.nick or irc.nick
                if msg.tagged('HtmlLogger__relayed'):
                    (nick, text) = text.split(' ', 1)
                    nick = nick[1:-1]
                    msg.args = (recipients, text)
                if (noLogPrefix and text.startswith(noLogPrefix)) or \
                        not logChannelMessages:
                    text = '-= THIS MESSAGE NOT LOGGED =-'
                if ircmsgs.isAction(msg):
                    self.doLog(irc, channel, False, None,
                               '* %s %s', nick, ircmsgs.unAction(msg))
                else:
                    self.doLog(irc, channel, False, nick, '%s', text)

    def doNotice(self, irc, msg):
        (recipients, text) = msg.args
        for channel in recipients.split(','):
            if irc.isChannel(channel):
                self.doLog(irc, channel, True, None, '-%s- %s', msg.nick, text)

    def doNick(self, irc, msg):
        oldNick = msg.nick
        newNick = msg.args[0]
        for (channel, c) in irc.state.channels.items():
            if newNick in c.users:
                self.doLog(irc, channel, True, None,
                           '*** %s is now known as %s', oldNick, newNick)
    def doJoin(self, irc, msg):
        for channel in msg.args[0].split(','):
            if(self.registryValue('showJoinParts', channel)):
                self.doLog(irc, channel, True, None,
                           '*** %s <%s> has joined %s',
                           msg.nick, msg.prefix, channel)

    def doKick(self, irc, msg):
        if len(msg.args) == 3:
            (channel, target, kickmsg) = msg.args
        else:
            (channel, target) = msg.args
            kickmsg = ''
        if kickmsg:
            self.doLog(irc, channel, True, None,
                       '*** %s was kicked by %s (%s)',
                       target, msg.nick, kickmsg)
        else:
            self.doLog(irc, channel, True, None,
                       '*** %s was kicked by %s', target, msg.nick)

    def doPart(self, irc, msg):
        if len(msg.args) > 1:
            reason = " (%s)" % msg.args[1]
        else:
            reason = ""
        for channel in msg.args[0].split(','):
            if(self.registryValue('showJoinParts', channel)):
                self.doLog(irc, channel, True, None,
                           '*** %s <%s> has left %s%s',
                           msg.nick, msg.prefix, channel, reason)

    def doMode(self, irc, msg):
        channel = msg.args[0]
        if irc.isChannel(channel) and msg.args[1:]:
            self.doLog(irc, channel, True, None,
                       '*** %s sets mode: %s %s',
                       msg.nick or msg.prefix, msg.args[1],
                        ' '.join(msg.args[2:]))

    def doTopic(self, irc, msg):
        if len(msg.args) == 1:
            return # It's an empty TOPIC just to get the current topic.
        channel = msg.args[0]
        self.doLog(irc, channel, True, None,
                   '*** %s changes topic to "%s"', msg.nick, msg.args[1])

    def doQuit(self, irc, msg):
        if len(msg.args) == 1:
            reason = " (%s)" % msg.args[0]
        else:
            reason = ""
        if not isinstance(irc, irclib.Irc):
            irc = irc.getRealIrc()
        for (channel, chan) in self.lastStates[irc].channels.items():
            if(self.registryValue('showJoinParts', channel)):
                if msg.nick in chan.users:
                    self.doLog(irc, channel, True, None,
                               '*** %s <%s> has quit IRC%s',
                               msg.nick, msg.prefix, reason)

    def outFilter(self, irc, msg):
        # Gotta catch my own messages *somehow* :)
        # Let's try this little trick...
        if msg.command in ('PRIVMSG', 'NOTICE'):
            # Other messages should be sent back to us.
            m = ircmsgs.IrcMsg(msg=msg, prefix=irc.prefix)
            if msg.tagged('relayedMsg'):
                m.tag('HtmlLogger__relayed')
            self(irc, m)
        return msg

Class = HtmlLogger
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2010-2012, GEM Foundation.
#
# OpenQuake is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OpenQuake 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 Affero General Public License
# along with OpenQuake.  If not, see <http://www.gnu.org/licenses/>.

"""
Serializer and related functions to save exposure data to the database.
"""

import os

from openquake.db import models
from django.db import router
from django.db import transaction

from openquake.parser import risk as exposure
from openquake import shapes

class ExposureDBWriter(object):
    """
    Serialize the exposure model to database
    """

    model_attrs = [
        ("area_type", "areaType"), ("area_unit", "areaUnit"),
        ("coco_type", "cocoType"), ("coco_unit", "cocoUnit"),
        ("reco_type", "recoType"), ("reco_unit", "recoUnit"),
        ("stco_type", "stcoType"), ("stco_unit", "stcoUnit"),
        ("unit_type", "unitType")]

    def __init__(self, smi, owner=None):
        """Create a new serializer for the specified user"""
        self.smi = smi
        if owner:
            self.owner = owner
        else:
            self.owner = smi.owner
        self.model = None

    @transaction.commit_on_success(router.db_for_write(models.ExposureModel))
    def serialize(self, iterator):
        """
        Serialize a list of values produced by
        :class:`openquake.parser.exposure.ExposureModelFile`

        :type iterator: any iterable
        """
        for point, occupancy, values in iterator:
            self.insert_datum(point, occupancy, values)

    def insert_datum(self, point, occupancy, values):
        """
        Insert a single asset entry.

        :param list point:
            Asset location (format [lon, lat]).
        :param list occupancy:
            A potentially empty list of named tuples
            each one having an 'occupants' and a 'description' property.
        :param dict values:
            Asset attributes (see
            :class:`nrml.risk.parsers.ExposureModelParser`).

        It also inserts the main exposure model entry if
        not already present.
        """

        if not self.model:
            self.model = models.ExposureModel(
                owner=self.owner, input=self.smi,
                description=values.get("listDescription"),
                taxonomy_source=values.get("taxonomySource"),
                category=values["assetCategory"])
            for key, tag in self.model_attrs:
                value = values.get(tag)
                if value:
                    setattr(self.model, key, value)
            self.model.save()

        data = models.ExposureData(
            exposure_model=self.model, asset_ref=values["assetID"],
            taxonomy=values.get("taxonomy"),
            site="POINT(%s %s)" % (point[0], point[1]))
        for key, tag in [
            ("coco", "coco"), ("reco", "reco"), ("stco", "stco"),
            ("area", "area"), ("number_of_units", "number"),
            ("deductible", "deductible"), ("ins_limit", "limit")]:
            value = values.get(tag)
            if value:
                setattr(data, key, value)
        data.save()
        for odata in occupancy:
            oobj = models.Occupancy(exposure_data=data,
                                    occupants=odata.occupants,
                                    category=odata.description)
            oobj.save()

def read_sites_from_exposure(job_ctxt):
    """
    Given a :class:`JobContext` object, get all of the sites in the exposure
    model which are contained by the region of interest (defined in the
    `JobContext`).

    It is assumed that exposure model is already loaded into the database.

    :param job_ctxt:
        :class:`JobContext` instance.
    :returns:
        `list` of :class:`openquake.shapes.Site` objects, with no duplicates
    """

    em_inputs = models.inputs4job(job_ctxt.job_id, input_type="exposure")
    exp_points = models.ExposureData.objects.filter(
        exposure_model__input__id__in=[em.id for em in em_inputs],
        site__contained=job_ctxt.oq_job_profile.region).values(
        'site').distinct()

    return [shapes.Site(p['site'].x, p['site'].y) for p in exp_points]

def store_exposure_assets(job_id, base_path):
    """
    Load exposure assets from input file and store them
    into database.

    If the given job already has an input of type `exposure`,
    this function simply returns without doing anything.

    :param job_id: the id of the job where the assets
        belong to.
    :type job_id: integer
    :param base_path: the path where the application has been
        triggered. It is used to properly locate the input
        files, that are stored with a relative path.
    :type base_path: string
    """

    [emi] = models.inputs4job(job_id, "exposure")
    if emi.exposuremodel_set.all().count() > 0:
        return

    path = os.path.join(base_path, emi.path)
    exposure_parser = exposure.ExposureModelFile(path)
    writer = ExposureDBWriter(emi)
    writer.serialize(exposure_parser)
<commit_msg>Removed unused code<commit_after># -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2010-2012, GEM Foundation.
#
# OpenQuake is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OpenQuake 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 Affero General Public License
# along with OpenQuake.  If not, see <http://www.gnu.org/licenses/>.

"""
Serializer and related functions to save exposure data to the database.
"""

from openquake.db import models
from django.db import router
from django.db import transaction

class ExposureDBWriter(object):
    """
    Serialize the exposure model to database
    """

    model_attrs = [
        ("area_type", "areaType"), ("area_unit", "areaUnit"),
        ("coco_type", "cocoType"), ("coco_unit", "cocoUnit"),
        ("reco_type", "recoType"), ("reco_unit", "recoUnit"),
        ("stco_type", "stcoType"), ("stco_unit", "stcoUnit"),
        ("unit_type", "unitType")]

    def __init__(self, smi, owner=None):
        """Create a new serializer for the specified user"""
        self.smi = smi
        if owner:
            self.owner = owner
        else:
            self.owner = smi.owner
        self.model = None

    @transaction.commit_on_success(router.db_for_write(models.ExposureModel))
    def serialize(self, iterator):
        """
        Serialize a list of values produced by
        :class:`openquake.parser.exposure.ExposureModelFile`

        :type iterator: any iterable
        """
        for point, occupancy, values in iterator:
            self.insert_datum(point, occupancy, values)

    def insert_datum(self, point, occupancy, values):
        """
        Insert a single asset entry.

        :param list point:
            Asset location (format [lon, lat]).
        :param list occupancy:
            A potentially empty list of named tuples
            each one having an 'occupants' and a 'description' property.
        :param dict values:
            Asset attributes (see
            :class:`nrml.risk.parsers.ExposureModelParser`).

        It also inserts the main exposure model entry if
        not already present.
        """

        if not self.model:
            self.model = models.ExposureModel(
                owner=self.owner, input=self.smi,
                description=values.get("listDescription"),
                taxonomy_source=values.get("taxonomySource"),
                category=values["assetCategory"])
            for key, tag in self.model_attrs:
                value = values.get(tag)
                if value:
                    setattr(self.model, key, value)
            self.model.save()

        data = models.ExposureData(
            exposure_model=self.model, asset_ref=values["assetID"],
            taxonomy=values.get("taxonomy"),
            site="POINT(%s %s)" % (point[0], point[1]))
        for key, tag in [
            ("coco", "coco"), ("reco", "reco"), ("stco", "stco"),
            ("area", "area"), ("number_of_units", "number"),
            ("deductible", "deductible"), ("ins_limit", "limit")]:
            value = values.get(tag)
            if value:
                setattr(data, key, value)
        data.save()
        for odata in occupancy:
            oobj = models.Occupancy(exposure_data=data,
                                    occupants=odata.occupants,
                                    category=odata.description)
            oobj.save()
<|endoftext|>