<commit_before>#!/usr/bin/env python2

from __future__ import absolute_import
from __future__ import print_function
import sys
import re
import six

class KernelConfig:

    def __init__(self):
        self.options = dict()
        self.options_match_distro = dict()
        self.old_options = dict()

    def parse(self, cfg):
        for line in cfg:
            line = line.strip()

            if not line:
                # print "skip empty line"
                continue

            if re.search("^#.*$", line):
                m = re.match("^# CONFIG_([^ ]+) is not set$", line)

                if m:
                    self.options[m.group(1)] = ("simple", "n")
                    # print "CONFIG " + m.group(1) + " is not set"
                else:
                    m = re.match("^# CONFIG_([^ ]+) matches distro$", line)

                    if m:
                        self.options_match_distro[m.group(1)] = True
                    else:
                        # print "skip comment: " + line
                        continue
                continue

            m = re.match("^CONFIG_([^=]+)=([ym])$", line)
            if m:
                self.options[m.group(1)] = ("simple", m.group(2))
                # print "CONFIG " + m.group(1) + " is set to " + m.group(2)
                continue

            m = re.match("^CONFIG_([^=]+)=([-]?[0-9]+)$", line)
            if m:
                self.options[m.group(1)] = ("number", m.group(2))
                # print "CONFIG " + m.group(1) + " is set to " + m.group(2)
                continue

            m = re.match("^CONFIG_([^=]+)=(0x[0-9a-f]+)$", line)
            if m:
                self.options[m.group(1)] = ("number", m.group(2))
                # print "CONFIG " + m.group(1) + " is set to " + m.group(2)
                continue

            m = re.match("^CONFIG_([^=]+)=\"(.*)\"$", line)
            if m:
                self.options[m.group(1)] = ("string", m.group(2))
                # print "CONFIG " + m.group(1) + " is set to " + m.group(2)
                continue

            print("unmatched line: " + line)

    def parse_file(self, filename):
        cfg = open(filename, 'r')
        self.parse(cfg)
        cfg.close()

    def value_to_string(self, value):
        if value[0] == "simple":
            return value[1]

        if value[0] == "number":
            return value[1]

        if value[0] == "string":
            return "\"" + value[1] + "\""

    def config_to_string(self, opt, value):
        option = "CONFIG_" + opt
        valstr = self.value_to_string(value)

        if value == ("simple", "n"):
            return "# " + option + " is not set"
        else:
            return option + "=" + valstr

    def store(self, cfg):
        for opt, value in sorted(self.options.items()):
            if opt in self.options_match_distro:
                option = "CONFIG_" + opt
                cfg.write("# " + option + " matches distro\n")
            cfg.write(self.config_to_string(opt, value) + "\n")

    def store_file(self, filename):
        cfg = open(filename, 'w')
        self.store(cfg)
        cfg.close()

    def store_diff(self, cfg):
        for opt, value in sorted(self.options.items()):
            optstr = "CONFIG_" + opt
            newval = self.value_to_string(value)
            oldval = self.value_to_string(self.old_options[opt])
            cfg.write(
                optstr + "=" + newval + " (changed from: " + oldval + ")\n")

    def store_diff_file(self, filename):
        cfg = open(filename, 'w')
        self.store_diff(cfg)
        cfg.close()

    def trim_by_dist_config(self, dist):
        trimmed = KernelConfig()

        # everything that user sets which is not in dist, or
        # differs from dist, will be included in trimmed config
        for opt, value in six.iteritems(self.options):
            trimmed.options[opt] = value
            if opt in dist.options and value == dist.options[opt]:
                trimmed.options_match_distro[opt] = True

        # everything that dist sets and is missing in user config,
        # will be explicitly disabled in trimmed config
        for opt in six.iterkeys(dist.options):
            if opt not in self.options:
                trimmed.options[opt] = ("simple", "n")

        return trimmed

    def combine_with_dist_config(self, dist):
        combined = KernelConfig()
        # start with everything from user config that didn't match
        # the corresponding distro config
        for opt, value in six.iteritems(self.options):
            if opt not in self.options_match_distro:
                combined.options[opt] = value

        # add values from new distro config
        for opt, value in six.iteritems(dist.options):
            if opt not in combined.options:
                combined.options[opt] = value

        return combined

    def compare_user_with_combined(self, comb, dist):
        was_disabled = KernelConfig()
        is_missing = KernelConfig()
        was_changed = KernelConfig()
        new_option = KernelConfig()
        dist_was_disabled = KernelConfig()
        dist_new_option = KernelConfig()
        dist_is_missing = KernelConfig()
        dist_was_changed = KernelConfig()

        # see what was changed against the user config
        for opt, value in six.iteritems(self.options):
            # options matching the old distro config are not important
            if opt in self.options_match_distro:
                continue
            if value == ("simple", "n"):
                # disabled options that disappeared are not important
                if opt not in comb.options:
                    continue
                # record disabled options that became enabled
                if value != comb.options[opt]:
                    was_disabled.options[opt] = comb.options[opt]
                    continue
            # record options that were enabled and are gone completely
            if opt not in comb.options:
                is_missing.options[opt] = value
                continue
            # record options whose value changed (including being disabled)
            if value != comb.options[opt]:
                was_changed.options[opt] = comb.options[opt]
                was_changed.old_options[opt] = value

        # see what was changed agains the new distro config
        for opt, value in six.iteritems(dist.options):
            # options handled by the user config diff are skipped here
            if opt in self.options and opt not in self.options_match_distro:
                continue
            # record options that were enabled and are gone completely
            if opt not in comb.options:
                if value != ("simple", "n"):
                    dist_is_missing.options[opt] = value
                continue
            # record options whose value changed (including being disabled)
            if value != comb.options[opt]:
                # record disabled options that became enabled
                if value == ("simple", "n"):
                    dist_was_disabled[opt] = comb.options[opt]
                else:
                    dist_was_changed.options[opt] = comb.options[opt]
                    dist_was_changed.old_options[opt] = value

        # record options that are new, and unknown by user config and/or distro
        # config
        for opt, value in six.iteritems(comb.options):
            # disabled options are unimportant
            if value == ("simple", "n"):
                continue
            # skip options known to user config (matching distro or not doesn't
            # matter)
            if opt in self.options:
                continue
            # completely new options had values taken from upstream KConfig
            # default
            if opt not in dist.options:
                new_option.options[opt] = value
            else:
                # the rest got value from distro config
                dist_new_option.options[opt] = value

        return (was_disabled, is_missing, was_changed, new_option, dist_new_option, dist_is_missing, dist_was_changed, dist_was_disabled)

action = sys.argv[1]

if action == "--trim":

    user_config = KernelConfig()
    dist_config = KernelConfig()

    user_config.parse_file(sys.argv[2])
    dist_config.parse_file(sys.argv[3])

    trim_config = user_config.trim_by_dist_config(dist_config)
    trim_config.store_file(sys.argv[4])

elif action == "--combine":

    user_config = KernelConfig()
    dist_config = KernelConfig()

    user_config.parse_file(sys.argv[2])
    dist_config.parse_file(sys.argv[3])

    comb_config = user_config.combine_with_dist_config(dist_config)
    comb_config.store_file(sys.argv[4])

elif action == "--diff":

    diff_path = sys.argv[5] + "/"

    user_config = KernelConfig()
    dist_config = KernelConfig()
    comb_config = KernelConfig()

    user_config.parse_file(sys.argv[2])
    dist_config.parse_file(sys.argv[3])
    comb_config.parse_file(sys.argv[4])

    (was_disabled, is_missing, was_changed, new_option, dist_new_option, dist_is_missing, dist_was_changed, dist_was_disabled) = \
        user_config.compare_user_with_combined(comb_config, dist_config)

    was_disabled.store_file(diff_path + "was_disabled")
    is_missing.store_file(diff_path + "is_missing")
    was_changed.store_diff_file(diff_path + "was_changed")
    new_option.store_file(diff_path + "new_options")
    dist_was_disabled.store_file(diff_path + "dist_was_disabled")
    dist_was_changed.store_diff_file(diff_path + "dist_was_changed")
    dist_is_missing.store_file(diff_path + "dist_is_missing")
    dist_new_option.store_file(diff_path + "dist_new_options")

else:
    sys.stderr.write("unknown action: " + action + "\n")
<commit_msg>tweak results of modernize towards Python 3.x support<commit_after>#!/usr/bin/python

import sys
import re

class KernelConfig:

    def __init__(self):
        self.options = dict()
        self.options_match_distro = dict()
        self.old_options = dict()

    def parse(self, cfg):
        for line in cfg:
            line = line.strip()

            if not line:
                # print "skip empty line"
                continue

            if re.search("^#.*$", line):
                m = re.match("^# CONFIG_([^ ]+) is not set$", line)

                if m:
                    self.options[m.group(1)] = ("simple", "n")
                    # print "CONFIG " + m.group(1) + " is not set"
                else:
                    m = re.match("^# CONFIG_([^ ]+) matches distro$", line)

                    if m:
                        self.options_match_distro[m.group(1)] = True
                    else:
                        # print "skip comment: " + line
                        continue
                continue

            m = re.match("^CONFIG_([^=]+)=([ym])$", line)
            if m:
                self.options[m.group(1)] = ("simple", m.group(2))
                # print "CONFIG " + m.group(1) + " is set to " + m.group(2)
                continue

            m = re.match("^CONFIG_([^=]+)=([-]?[0-9]+)$", line)
            if m:
                self.options[m.group(1)] = ("number", m.group(2))
                # print "CONFIG " + m.group(1) + " is set to " + m.group(2)
                continue

            m = re.match("^CONFIG_([^=]+)=(0x[0-9a-f]+)$", line)
            if m:
                self.options[m.group(1)] = ("number", m.group(2))
                # print "CONFIG " + m.group(1) + " is set to " + m.group(2)
                continue

            m = re.match("^CONFIG_([^=]+)=\"(.*)\"$", line)
            if m:
                self.options[m.group(1)] = ("string", m.group(2))
                # print "CONFIG " + m.group(1) + " is set to " + m.group(2)
                continue

            print("unmatched line: " + line)

    def parse_file(self, filename):
        cfg = open(filename, 'r')
        self.parse(cfg)
        cfg.close()

    def value_to_string(self, value):
        if value[0] == "simple":
            return value[1]

        if value[0] == "number":
            return value[1]

        if value[0] == "string":
            return "\"" + value[1] + "\""

    def config_to_string(self, opt, value):
        option = "CONFIG_" + opt
        valstr = self.value_to_string(value)

        if value == ("simple", "n"):
            return "# " + option + " is not set"
        else:
            return option + "=" + valstr

    def store(self, cfg):
        for opt, value in sorted(self.options.items()):
            if opt in self.options_match_distro:
                option = "CONFIG_" + opt
                cfg.write("# " + option + " matches distro\n")
            cfg.write(self.config_to_string(opt, value) + "\n")

    def store_file(self, filename):
        cfg = open(filename, 'w')
        self.store(cfg)
        cfg.close()

    def store_diff(self, cfg):
        for opt, value in sorted(self.options.items()):
            optstr = "CONFIG_" + opt
            newval = self.value_to_string(value)
            oldval = self.value_to_string(self.old_options[opt])
            cfg.write(
                optstr + "=" + newval + " (changed from: " + oldval + ")\n")

    def store_diff_file(self, filename):
        cfg = open(filename, 'w')
        self.store_diff(cfg)
        cfg.close()

    def trim_by_dist_config(self, dist):
        trimmed = KernelConfig()

        # everything that user sets which is not in dist, or
        # differs from dist, will be included in trimmed config
        for opt, value in self.options.items():
            trimmed.options[opt] = value
            if opt in dist.options and value == dist.options[opt]:
                trimmed.options_match_distro[opt] = True

        # everything that dist sets and is missing in user config,
        # will be explicitly disabled in trimmed config
        for opt in dist.options.keys():
            if opt not in self.options:
                trimmed.options[opt] = ("simple", "n")

        return trimmed

    def combine_with_dist_config(self, dist):
        combined = KernelConfig()
        # start with everything from user config that didn't match
        # the corresponding distro config
        for opt, value in self.options.items():
            if opt not in self.options_match_distro:
                combined.options[opt] = value

        # add values from new distro config
        for opt, value in dist.options.items():
            if opt not in combined.options:
                combined.options[opt] = value

        return combined

    def compare_user_with_combined(self, comb, dist):
        was_disabled = KernelConfig()
        is_missing = KernelConfig()
        was_changed = KernelConfig()
        new_option = KernelConfig()
        dist_was_disabled = KernelConfig()
        dist_new_option = KernelConfig()
        dist_is_missing = KernelConfig()
        dist_was_changed = KernelConfig()

        # see what was changed against the user config
        for opt, value in self.options.items():
            # options matching the old distro config are not important
            if opt in self.options_match_distro:
                continue
            if value == ("simple", "n"):
                # disabled options that disappeared are not important
                if opt not in comb.options:
                    continue
                # record disabled options that became enabled
                if value != comb.options[opt]:
                    was_disabled.options[opt] = comb.options[opt]
                    continue
            # record options that were enabled and are gone completely
            if opt not in comb.options:
                is_missing.options[opt] = value
                continue
            # record options whose value changed (including being disabled)
            if value != comb.options[opt]:
                was_changed.options[opt] = comb.options[opt]
                was_changed.old_options[opt] = value

        # see what was changed agains the new distro config
        for opt, value in dist.options.items():
            # options handled by the user config diff are skipped here
            if opt in self.options and opt not in self.options_match_distro:
                continue
            # record options that were enabled and are gone completely
            if opt not in comb.options:
                if value != ("simple", "n"):
                    dist_is_missing.options[opt] = value
                continue
            # record options whose value changed (including being disabled)
            if value != comb.options[opt]:
                # record disabled options that became enabled
                if value == ("simple", "n"):
                    dist_was_disabled[opt] = comb.options[opt]
                else:
                    dist_was_changed.options[opt] = comb.options[opt]
                    dist_was_changed.old_options[opt] = value

        # record options that are new, and unknown by user config and/or distro
        # config
        for opt, value in comb.options.items():
            # disabled options are unimportant
            if value == ("simple", "n"):
                continue
            # skip options known to user config (matching distro or not doesn't
            # matter)
            if opt in self.options:
                continue
            # completely new options had values taken from upstream KConfig
            # default
            if opt not in dist.options:
                new_option.options[opt] = value
            else:
                # the rest got value from distro config
                dist_new_option.options[opt] = value

        return (was_disabled, is_missing, was_changed, new_option, dist_new_option, dist_is_missing, dist_was_changed, dist_was_disabled)

action = sys.argv[1]

if action == "--trim":

    user_config = KernelConfig()
    dist_config = KernelConfig()

    user_config.parse_file(sys.argv[2])
    dist_config.parse_file(sys.argv[3])

    trim_config = user_config.trim_by_dist_config(dist_config)
    trim_config.store_file(sys.argv[4])

elif action == "--combine":

    user_config = KernelConfig()
    dist_config = KernelConfig()

    user_config.parse_file(sys.argv[2])
    dist_config.parse_file(sys.argv[3])

    comb_config = user_config.combine_with_dist_config(dist_config)
    comb_config.store_file(sys.argv[4])

elif action == "--diff":

    diff_path = sys.argv[5] + "/"

    user_config = KernelConfig()
    dist_config = KernelConfig()
    comb_config = KernelConfig()

    user_config.parse_file(sys.argv[2])
    dist_config.parse_file(sys.argv[3])
    comb_config.parse_file(sys.argv[4])

    (was_disabled, is_missing, was_changed, new_option, dist_new_option, dist_is_missing, dist_was_changed, dist_was_disabled) = \
        user_config.compare_user_with_combined(comb_config, dist_config)

    was_disabled.store_file(diff_path + "was_disabled")
    is_missing.store_file(diff_path + "is_missing")
    was_changed.store_diff_file(diff_path + "was_changed")
    new_option.store_file(diff_path + "new_options")
    dist_was_disabled.store_file(diff_path + "dist_was_disabled")
    dist_was_changed.store_diff_file(diff_path + "dist_was_changed")
    dist_is_missing.store_file(diff_path + "dist_is_missing")
    dist_new_option.store_file(diff_path + "dist_new_options")

else:
    sys.stderr.write("unknown action: " + action + "\n")
<|endoftext|>


<commit_before>from plow.gui.manifest import QtCore, QtGui
from plow.gui.util import formatDateTime, formatDuration

__all__ = [
    "Text",
    "Number",
    "DateTime"
]

class FormWidget(QtGui.QWidget):
    """
    The base class for all form widgets.
    """
    __LOCKED_PIX = None

    def __init__(self, value, parent=None):
        QtGui.QWidget.__init__(self, parent)
        layout = QtGui.QGridLayout(self)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self._widget = None

        self.__status = QtGui.QLabel(self)
        self.__status.setContentsMargins(5, 0, 0, 0)
        layout.addWidget(self.__status, 0, 1)

        if not FormWidget.__LOCKED_PIX:
            FormWidget.__LOCKED_PIX = QtGui.QPixmap(":/images/locked.png")
            FormWidget.__LOCKED_PIX = FormWidget.__LOCKED_PIX.scaled(
                QtCore.QSize(12, 12), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)

    def setReadOnly(self, value):
        self._setReadOnly(value)
        if value:
            self.__status.setPixmap(FormWidget.__LOCKED_PIX)
        else:
            self.__status.setText("")

    def _setReadOnly(self, value):
        pass

    def setWidget(self, widget):
        self._widget = widget
        self.layout().addWidget(widget, 0, 0)

class Text(FormWidget):
    def __init__(self, text, parent=None):
        FormWidget.__init__(self, parent)
        self.setWidget(QtGui.QLineEdit(text, self))
        self._widget.setFocusPolicy(QtCore.Qt.NoFocus)

    def _setReadOnly(self, value):
        self._widget.setReadOnly(value)

class Number(FormWidget):
    def __init__(self, value, parent=None):
        FormWidget.__init__(self, parent)
        widget = QtGui.QSpinBox(self)
        widget.setValue(value)
        self.setWidget(widget)
        self._widget.setFocusPolicy(QtCore.Qt.NoFocus)

    def _setReadOnly(self, value):
        self._widget.setReadOnly(value)

class DateTime(FormWidget):
    def __init__(self, value, parent=None):
        FormWidget.__init__(self, parent)
        self.setWidget(QtGui.QLabel(formatDateTime(value), self))

class Duration(FormWidget):
    def __init__(self, times, parent=None):
        FormWidget.__init__(self, parent)
        self.setWidget(QtGui.QLabel(formatDuration(times[0], times[1]), self))
<commit_msg>Added some new form widgets.<commit_after>from plow.gui.manifest import QtCore, QtGui
from plow.gui.util import formatDateTime, formatDuration

__all__ = [
    "Text",
    "Number",
    "Decimal",
    "DateTime",
    "PillWidget"
]

class FormWidget(QtGui.QWidget):
    """
    The base class for all form widgets.
    """
    __LOCKED_PIX = None

    def __init__(self, value, parent=None):
        QtGui.QWidget.__init__(self, parent)
        layout = QtGui.QGridLayout(self)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self._widget = None

        self.__status = QtGui.QLabel(self)
        self.__status.setContentsMargins(5, 0, 0, 0)
        layout.addWidget(self.__status, 0, 1)

        if not FormWidget.__LOCKED_PIX:
            FormWidget.__LOCKED_PIX = QtGui.QPixmap(":/images/locked.png")
            FormWidget.__LOCKED_PIX = FormWidget.__LOCKED_PIX.scaled(
                QtCore.QSize(12, 12), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)

    def setReadOnly(self, value):
        self._setReadOnly(value)
        if value:
            self.__status.setPixmap(FormWidget.__LOCKED_PIX)
        else:
            self.__status.setText("")

    def _setReadOnly(self, value):
        pass

    def setWidget(self, widget):
        self._widget = widget
        self.layout().addWidget(widget, 0, 0)

class Text(FormWidget):
    def __init__(self, text, parent=None):
        FormWidget.__init__(self, parent)
        self.setWidget(QtGui.QLineEdit(text, self))
        self._widget.setFocusPolicy(QtCore.Qt.NoFocus)

    def _setReadOnly(self, value):
        self._widget.setReadOnly(value)

class Number(FormWidget):
    def __init__(self, value, parent=None):
        FormWidget.__init__(self, parent)
        widget = QtGui.QSpinBox(self)
        widget.setValue(value)
        self.setWidget(widget)
        self._widget.setFocusPolicy(QtCore.Qt.NoFocus)

    def _setReadOnly(self, value):
        self._widget.setReadOnly(value)
        self._widget.setButtonSymbols(QtGui.QAbstractSpinBox.NoButtons)

class Decimal(FormWidget):
    def __init__(self, value, parent=None):
        FormWidget.__init__(self, parent)
        widget = QtGui.QDoubleSpinBox(self)
        widget.setValue(value)
        self.setWidget(widget)
        self._widget.setFocusPolicy(QtCore.Qt.NoFocus)

    def _setReadOnly(self, value):
        self._widget.setReadOnly(value)
        self._widget.setButtonSymbols(QtGui.QAbstractSpinBox.NoButtons)

class DateTime(FormWidget):
    def __init__(self, value, parent=None):
        FormWidget.__init__(self, parent)
        self.setWidget(QtGui.QLabel(formatDateTime(value), self))

class Duration(FormWidget):
    def __init__(self, times, parent=None):
        FormWidget.__init__(self, parent)
        self.setWidget(QtGui.QLabel(formatDuration(times[0], times[1]), self))

class PillWidget(FormWidget):
    def __init__(self, value, parent):
        FormWidget.__init__(self, parent)
        data, color = value

        self.label = QtGui.QLabel(data, self)
        self.label.setStyleSheet("border: 1px solid #222222; background-color: %s; border-radius: 6px;" % color)
        self.label.setMinimumWidth(100)
        self.setWidget(self.label)<|endoftext|>


<commit_before># This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (C) 2018, James R. Barlow (https://github.com/jbarlow83/)

from collections.abc import MutableMapping
from datetime import datetime
from functools import wraps
from itertools import groupby
from pkg_resources import (
    get_distribution as _get_distribution,
    DistributionNotFound
)
from warnings import warn

from libxmp import XMPMeta, XMPIterator
from libxmp.consts import (
    XMP_NS_DC, XMP_NS_PDF, XMP_NS_PDFA_ID, XMP_NS_PDFX_ID, XMP_NS_RDF, XMP_NS_XMP
)

from .. import Stream, Name, String

# Repeat this to avoid circular from top package's pikepdf.__version__
try:
    pikepdf_version = _get_distribution(__name__).version
except DistributionNotFound:
    pikepdf_version = "unknown version"

def encode_pdf_date(d: datetime) -> str:
    """Encode Python datetime object as PDF date string

    From Adobe pdfmark manual:
    (D:YYYYMMDDHHmmSSOHH'mm')
    D: is an optional prefix. YYYY is the year. All fields after the year are
    optional. MM is the month (01-12), DD is the day (01-31), HH is the
    hour (00-23), mm are the minutes (00-59), and SS are the seconds
    (00-59). The remainder of the string defines the relation of local
    time to GMT. O is either + for a positive difference (local time is
    later than GMT) or - (minus) for a negative difference. HH' is the
    absolute value of the offset from GMT in hours, and mm' is the
    absolute value of the offset in minutes. If no GMT information is
    specified, the relation between the specified time and GMT is
    considered unknown. Regardless of whether or not GMT
    information is specified, the remainder of the string should specify
    the local time.
    """

    pdfmark_date_fmt = r'%Y%m%d%H%M%S'
    s = d.strftime(pdfmark_date_fmt)
    tz = d.strftime('%z')
    if tz == '':
        # Ghostscript <= 9.23 handles missing timezones incorrectly, so if
        # timezone is missing, move it into GMT.
        # https://bugs.ghostscript.com/show_bug.cgi?id=699182
        s += "+00'00'"
    else:
        sign, tz_hours, tz_mins = tz[0], tz[1:3], tz[3:5]
        s += "{}{}'{}'".format(sign, tz_hours, tz_mins)
    return s

def decode_pdf_date(s: str) -> datetime:
    """Decode a pdfmark date to a Python datetime object

    A pdfmark date is a string in a paritcular format. See the pdfmark
    Reference for the specification.
    """
    if isinstance(s, String):
        s = str(s)
    if s.startswith('D:'):
        s = s[2:]

    # Literal Z00'00', is incorrect but found in the wild,
    # probably made by OS X Quartz -- standardize
    if s.endswith("Z00'00'"):
        s = s.replace("Z00'00'", '+0000')
    elif s.endswith('Z'):
        s = s.replace('Z', '+0000')
    s = s.replace("'", "")  # Remove apos from PDF time strings
    return datetime.strptime(s, r'%Y%m%d%H%M%S%z')

class AuthorConverter:
    @staticmethod
    def xmp_from_docinfo(docinfo_val):
        return str(docinfo_val)

    @staticmethod
    def docinfo_from_xmp(xmp_val):
        if isinstance(xmp_val, str):
            return xmp_val
        else:
            return '; '.join(xmp_val)

class DateConverter:
    @staticmethod
    def xmp_from_docinfo(docinfo_val):
        return decode_pdf_date(docinfo_val).isoformat()

    @staticmethod
    def docinfo_from_xmp(xmp_val):
        dateobj = datetime.fromisoformat(xmp_val)
        return encode_pdf_date(dateobj)

def ensure_loaded(fn):
    @wraps(fn)
    def wrapper(self, *args, **kwargs):
        if not self._records:
            self._load()
        return fn(self, *args, **kwargs)
    return wrapper

class PdfMetadata(MutableMapping):
    """Read and edit the XMP metadata associated with a PDF

    Requires/relies on python-xmp-toolkit and libexempi.

    To update metadata, use a with block.

    .. code-block:: python

        with pdf.open_metadata() as records:
            records['dc:title'] = 'New Title'

    See Also:
        :meth:`pikepdf.Pdf.open_metadata`
    """

    DEFAULT_NAMESPACES = [
        (XMP_NS_DC, 'dc'),
        (XMP_NS_PDF, 'pdf'),
        (XMP_NS_RDF, 'rdf'),
        (XMP_NS_XMP, 'xmp'),
    ]

    MAPPING = [
        (XMP_NS_DC, 'creator', Name.Authors, AuthorConverter),
        (XMP_NS_DC, 'description', Name.Subject, None),
        (XMP_NS_DC, 'title', Name.Title, None),
        (XMP_NS_PDF, 'Keywords', Name.Keywords, None),
        (XMP_NS_PDF, 'Producer', Name.Producer, None),
        (XMP_NS_XMP, 'CreateDate', Name.CreationDate, DateConverter),
        (XMP_NS_XMP, 'CreatorTool', Name.Creator, None),
        (XMP_NS_XMP, 'ModifyDate', Name.ModDate, DateConverter),
    ]

    def __init__(self, pdf, pikepdf_mark=True, sync_docinfo=True):
        self._pdf = pdf
        self._xmp = None
        self._records = {}
        self._flags = {}
        self.mark = pikepdf_mark
        self.sync_docinfo = sync_docinfo
        self._updating = False
        self._deleted = set()
        self._changed = set()

    def _create_xmp(self):
        self._xmp = XMPMeta()
        for uri, prefix in self.DEFAULT_NAMESPACES:
            self._xmp.register_namespace(uri, prefix)

    def load_from_docinfo(self, docinfo):
        """Populate the XMP metadata object with DocumentInfo

        A few entries in the deprecated DocumentInfo dictionary are considered
        approximately equivalent to certain XMP records. This method copies
        those entries into the XMP metadata.
        """
        for uri, shortkey, docinfo_name, converter in self.MAPPING:
            prefix = self._xmp.get_prefix_for_namespace(uri)
            key = prefix + shortkey
            val = docinfo.get(docinfo_name)
            if val is None:
                continue
            val = str(val)
            if converter:
                val = converter.xmp_from_docinfo(val)
            self[key] = val

    def _load(self):
        try:
            data = self._pdf.Root.Metadata.read_bytes()
        except AttributeError:
            self._create_xmp()
        else:
            self._xmp = XMPMeta(xmp_str=data.decode('utf-8'))

        def xmp_group_key(k):
            _schema, prop_name, _value, _flags = k
            return prop_name.split('[', maxsplit=1)[0]

        def xmp_item_index(k):
            _schema, prop_name, _value, _flags = k
            _compound_key, rest = prop_name.split('[', maxsplit=1)
            index = rest.split(']', maxsplit=1)[0]
            return index

        self._deleted = set()
        self._changed = set()
        xmpiter = XMPIterator(self._xmp)
        for prop_name_group, elements in groupby(xmpiter, key=xmp_group_key):
            elements = list(elements)
            parent = next(el for el in elements if el[1] == prop_name_group)
            elements.remove(parent)
            parent_value = parent[2]
            parent_flags = parent[3]

            if parent_flags['VALUE_IS_ARRAY']:
                if parent_flags['ARRAY_IS_ALTTEXT']:
                    record = []
                    for _index, subitems in groupby(elements, key=xmp_item_index):
                        subitems = list(subitems)
                        qualifier = next(el for el in subitems if el[3]['IS_QUALIFIER'])
                        text = next(el for el in subitems if el[3]['HAS_QUALIFIERS'])
                        if qualifier != 'x-default':
                            warn(
                                ("XMP metadata key {} has alternate language text. "
                                "This is not fully supported. If modified they "
                                "will not be preserved.").format(prop_name_group)
                            )
                        record = text[2]
                elif parent_flags['ARRAY_IS_ORDERED']:
                    record = [el[2] for el in elements]
                else:
                    record = set(el[2] for el in elements)
            elif parent_flags['VALUE_IS_STRUCT']:
                raise NotImplementedError()
            elif parent_value == '':
                continue
            else:
                record = parent[2]

            self._flags[prop_name_group] = parent_flags
            self._records[prop_name_group] = record
        return

    @ensure_loaded
    def __enter__(self):
        self._updating = True
        return self

    @staticmethod
    def _property_options(flags):
        return {('prop_' + k.lower()): v for k, v in flags.items()}

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            if exc_type is not None:
                return
            self._apply_changes()
        finally:
            self._records = {}
            self._deleted = set()
            self._changed = set()
            self._updating = False

    def _update_docinfo(self):
        """Update the PDF's DocumentInfo dictionary to match XMP metadata

        The standard mapping is described here:
            https://www.pdfa.org/pdfa-metadata-xmp-rdf-dublin-core/
        """

        for schema, element, docinfo_name, converter in self.MAPPING:
            prefix = self._xmp.get_prefix_for_namespace(schema)
            try:
                value = self._records[prefix + element]
            except KeyError:
                if docinfo_name in self._pdf.docinfo:
                    del self._pdf.docinfo[docinfo_name]
                continue
            if converter:
                value = converter.docinfo_from_xmp(value)
            self._pdf.docinfo[docinfo_name] = value

    def _get_uri(self, key):
        prefix = key.split(':', maxsplit=1)[0]
        return self._xmp.get_namespace_for_prefix(prefix)

    def _apply_changes(self):
        for key in self._deleted:
            uri = self._get_uri(key)
            self._xmp.delete_property(uri, key)

        for key in self._changed:
            val = self._records[key]
            uri = self._get_uri(key)
            if isinstance(val, (list, set, dict)):
                self._xmp.delete_property(uri, key)
                array_options = self._property_options(self._flags[key])
                for item in val:
                    self._xmp.append_array_item(uri, key, item, array_options=array_options)
            elif key in self._flags and self._flags[key]['ARRAY_IS_ALTTEXT']:
                self._xmp.delete_property(uri, key)
                self._xmp.set_property(uri, key, val)
            else:
                self._xmp.set_property(uri, key, val)

        if self.mark:
            self._xmp.set_property_datetime(
                XMP_NS_XMP, 'MetadataDate', datetime.now()
            )
            self._xmp.set_property(
                XMP_NS_PDF, 'Producer', 'pikepdf ' + pikepdf_version
            )

        data = self._xmp.serialize_to_unicode()
        self._pdf.Root.Metadata = Stream(self._pdf, data.encode('utf-8'))
        self._pdf.Root.Metadata[Name.Type] = Name.Metadata
        self._pdf.Root.Metadata[Name.Subtype] = Name.XML
        if self.sync_docinfo:
            self._update_docinfo()

    @ensure_loaded
    def __contains__(self, key):
        return key in self._records

    @ensure_loaded
    def __len__(self):
        return len(self._records)

    @ensure_loaded
    def __getitem__(self, key):
        return self._records[key]

    @ensure_loaded
    def __iter__(self):
        return iter(self._records)

    def _expected_type(self, key, val=None):
        if key not in self._flags:
            if isinstance(val, (list, set, dict, str)):
                return type(val)
            raise TypeError(val)
        if self._flags[key]['ARRAY_IS_ALTTEXT']:
            return str
        if self._flags[key]['VALUE_IS_ARRAY']:
            if self._flags[key]['ARRAY_IS_ORDERED']:
                return list
            return set
        if self._flags[key]['VALUE_IS_STRUCT']:
            return dict
        return str

    @ensure_loaded
    def __setitem__(self, key, val):
        if not self._updating:
            raise RuntimeError("Metadata not opened for editing, use with block")
        if not isinstance(val, self._expected_type(key, val)):
            raise TypeError("Invalid type set for metadata {}".format(key))
        self._changed.add(key)
        self._records[key] = val

    @ensure_loaded
    def __delitem__(self, key):
        if not self._updating:
            raise RuntimeError("Metadata not opened for editing, use with block")
        del self._records[key]
        self._deleted.add(key)

    @property
    @ensure_loaded
    def pdfa_status(self):
        """Returns the PDF/A conformance level claimed by this PDF, or False

        A PDF may claim to PDF/A compliant without this being true. Use an
        independent verifier such as veraPDF to test if a PDF is truly
        conformant.

        Returns:
            str: The conformance level of the PDF/A, or an empty string if the
            PDF does not claim PDF/A conformance. Possible valid values
            are: 1A, 1B, 2A, 2B, 2U, 3A, 3B, 3U.
        """
        pdfaid = self._xmp.get_prefix_for_namespace(XMP_NS_PDFA_ID)
        key_part = pdfaid + 'part'
        key_conformance = pdfaid + 'conformance'
        try:
            return self._records[key_part] + self._records[key_conformance]
        except KeyError:
            return ''

    @property
    @ensure_loaded
    def pdfx_status(self):
        """Returns the PDF/X conformance level claimed by this PDF, or False

        A PDF may claim to PDF/X compliant without this being true. Use an
        independent verifier such as veraPDF to test if a PDF is truly
        conformant.

        Returns:
            str: The conformance level of the PDF/X, or an empty string if the
            PDF does not claim PDF/X conformance.
        """
        pdfxid = self._xmp.get_prefix_for_namespace(XMP_NS_PDFX_ID)
        pdfx_version = pdfxid + 'GTS_PDFXVersion'
        try:
            return self._records[pdfx_version]
        except KeyError:
            return ''
<commit_msg>First stab<commit_after># This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (C) 2018, James R. Barlow (https://github.com/jbarlow83/)

from collections.abc import MutableMapping
from datetime import datetime
from functools import wraps
from itertools import groupby
from pkg_resources import (
    get_distribution as _get_distribution,
    DistributionNotFound
)
from warnings import warn
import xml.etree.ElementTree as ET

from libxmp import XMPMeta, XMPIterator
from .. import Stream, Name, String

XMP_NS_DC = "http://purl.org/dc/elements/1.1/"
XMP_NS_PDF = "http://ns.adobe.com/pdf/1.3/"
XMP_NS_PDFA_ID = "http://www.aiim.org/pdfa/ns/id/"
XMP_NS_PDFX_ID = "http://www.npes.org/pdfx/ns/id/"
XMP_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
XMP_NS_XMP = "http://ns.adobe.com/xap/1.0/"

DEFAULT_NAMESPACES = [
    (XMP_NS_DC, 'dc'),
    (XMP_NS_PDF, 'pdf'),
    (XMP_NS_RDF, 'rdf'),
    (XMP_NS_XMP, 'xmp'),
]

# Repeat this to avoid circular from top package's pikepdf.__version__
try:
    pikepdf_version = _get_distribution(__name__).version
except DistributionNotFound:
    pikepdf_version = "unknown version"

def encode_pdf_date(d: datetime) -> str:
    """Encode Python datetime object as PDF date string

    From Adobe pdfmark manual:
    (D:YYYYMMDDHHmmSSOHH'mm')
    D: is an optional prefix. YYYY is the year. All fields after the year are
    optional. MM is the month (01-12), DD is the day (01-31), HH is the
    hour (00-23), mm are the minutes (00-59), and SS are the seconds
    (00-59). The remainder of the string defines the relation of local
    time to GMT. O is either + for a positive difference (local time is
    later than GMT) or - (minus) for a negative difference. HH' is the
    absolute value of the offset from GMT in hours, and mm' is the
    absolute value of the offset in minutes. If no GMT information is
    specified, the relation between the specified time and GMT is
    considered unknown. Regardless of whether or not GMT
    information is specified, the remainder of the string should specify
    the local time.
    """

    pdfmark_date_fmt = r'%Y%m%d%H%M%S'
    s = d.strftime(pdfmark_date_fmt)
    tz = d.strftime('%z')
    if tz == '':
        # Ghostscript <= 9.23 handles missing timezones incorrectly, so if
        # timezone is missing, move it into GMT.
        # https://bugs.ghostscript.com/show_bug.cgi?id=699182
        s += "+00'00'"
    else:
        sign, tz_hours, tz_mins = tz[0], tz[1:3], tz[3:5]
        s += "{}{}'{}'".format(sign, tz_hours, tz_mins)
    return s

def decode_pdf_date(s: str) -> datetime:
    """Decode a pdfmark date to a Python datetime object

    A pdfmark date is a string in a paritcular format. See the pdfmark
    Reference for the specification.
    """
    if isinstance(s, String):
        s = str(s)
    if s.startswith('D:'):
        s = s[2:]

    # Literal Z00'00', is incorrect but found in the wild,
    # probably made by OS X Quartz -- standardize
    if s.endswith("Z00'00'"):
        s = s.replace("Z00'00'", '+0000')
    elif s.endswith('Z'):
        s = s.replace('Z', '+0000')
    s = s.replace("'", "")  # Remove apos from PDF time strings
    return datetime.strptime(s, r'%Y%m%d%H%M%S%z')

class AuthorConverter:
    @staticmethod
    def xmp_from_docinfo(docinfo_val):
        return str(docinfo_val)

    @staticmethod
    def docinfo_from_xmp(xmp_val):
        if isinstance(xmp_val, str):
            return xmp_val
        else:
            return '; '.join(xmp_val)

class DateConverter:
    @staticmethod
    def xmp_from_docinfo(docinfo_val):
        return decode_pdf_date(docinfo_val).isoformat()

    @staticmethod
    def docinfo_from_xmp(xmp_val):
        dateobj = datetime.fromisoformat(xmp_val)
        return encode_pdf_date(dateobj)

def ensure_loaded(fn):
    @wraps(fn)
    def wrapper(self, *args, **kwargs):
        if not self._xmp:
            self._load()
        return fn(self, *args, **kwargs)
    return wrapper

class PdfMetadata(MutableMapping):
    """Read and edit the XMP metadata associated with a PDF

    Requires/relies on python-xmp-toolkit and libexempi.

    To update metadata, use a with block.

    .. code-block:: python

        with pdf.open_metadata() as records:
            records['dc:title'] = 'New Title'

    See Also:
        :meth:`pikepdf.Pdf.open_metadata`
    """

    MAPPING = [
        (XMP_NS_DC, 'creator', Name.Authors, AuthorConverter),
        (XMP_NS_DC, 'description', Name.Subject, None),
        (XMP_NS_DC, 'title', Name.Title, None),
        (XMP_NS_PDF, 'Keywords', Name.Keywords, None),
        (XMP_NS_PDF, 'Producer', Name.Producer, None),
        (XMP_NS_XMP, 'CreateDate', Name.CreationDate, DateConverter),
        (XMP_NS_XMP, 'CreatorTool', Name.Creator, None),
        (XMP_NS_XMP, 'ModifyDate', Name.ModDate, DateConverter),
    ]

    def __init__(self, pdf, pikepdf_mark=True, sync_docinfo=True):
        self._pdf = pdf
        self._xmp = None
        self._ns = {prefix: uri for uri, prefix in DEFAULT_NAMESPACES}
        self._records = {}
        self._flags = {}
        self.mark = pikepdf_mark
        self.sync_docinfo = sync_docinfo
        self._updating = False
        self._deleted = set()
        self._changed = set()

    def _create_xmp(self):
        self._xmp = XMPMeta()

    def load_from_docinfo(self, docinfo):
        """Populate the XMP metadata object with DocumentInfo

        A few entries in the deprecated DocumentInfo dictionary are considered
        approximately equivalent to certain XMP records. This method copies
        those entries into the XMP metadata.
        """
        for uri, shortkey, docinfo_name, converter in self.MAPPING:
            prefix = self._xmp.get_prefix_for_namespace(uri)
            key = prefix + shortkey
            val = docinfo.get(docinfo_name)
            if val is None:
                continue
            val = str(val)
            if converter:
                val = converter.xmp_from_docinfo(val)
            self[key] = val

    def _load(self):
        try:
            data = self._pdf.Root.Metadata.read_bytes()
        except AttributeError:
            self._create_xmp()
        else:
            self._xmp = ET.fromstring(data)

    @ensure_loaded
    def __enter__(self):
        self._updating = True
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            if exc_type is not None:
                return
            self._apply_changes()
        finally:
            self._records = {}
            self._deleted = set()
            self._changed = set()
            self._updating = False

    def _update_docinfo(self):
        """Update the PDF's DocumentInfo dictionary to match XMP metadata

        The standard mapping is described here:
            https://www.pdfa.org/pdfa-metadata-xmp-rdf-dublin-core/
        """

        for schema, element, docinfo_name, converter in self.MAPPING:
            prefix = self._xmp.get_prefix_for_namespace(schema)
            try:
                value = self._records[prefix + element]
            except KeyError:
                if docinfo_name in self._pdf.docinfo:
                    del self._pdf.docinfo[docinfo_name]
                continue
            if converter:
                value = converter.docinfo_from_xmp(value)
            self._pdf.docinfo[docinfo_name] = value

    def _get_uri(self, key):
        prefix = key.split(':', maxsplit=1)[0]
        return self._xmp.get_namespace_for_prefix(prefix)

    def _apply_changes(self):
        if self.mark:
            self._xmp.set_property_datetime(
                XMP_NS_XMP, 'MetadataDate', datetime.now()
            )
            self._xmp.set_property(
                XMP_NS_PDF, 'Producer', 'pikepdf ' + pikepdf_version
            )

        data = self._xmp.serialize_to_unicode()
        self._pdf.Root.Metadata = Stream(self._pdf, data.encode('utf-8'))
        self._pdf.Root.Metadata[Name.Type] = Name.Metadata
        self._pdf.Root.Metadata[Name.Subtype] = Name.XML
        if self.sync_docinfo:
            self._update_docinfo()

    @ensure_loaded
    def __contains__(self, key):
        return bool(self._xmp.find('.//{}'.format(key), self._ns))

    @ensure_loaded
    def __len__(self):
        return len(self._xmp.find('.//{}'.format(key), self._ns))

    @ensure_loaded
    def __getitem__(self, key):
        element = self._xmp.find('.//{}'.format(key), self._ns)
        has_children = bool(element.find('.//', self._ns))
        if has_children:
            raise NotImplementedError()
        return element.tag

    @ensure_loaded
    def __iter__(self):
        return iter(self._records)

    def _expected_type(self, key, val=None):
        if key not in self._flags:
            if isinstance(val, (list, set, dict, str)):
                return type(val)
            raise TypeError(val)
        if self._flags[key]['ARRAY_IS_ALTTEXT']:
            return str
        if self._flags[key]['VALUE_IS_ARRAY']:
            if self._flags[key]['ARRAY_IS_ORDERED']:
                return list
            return set
        if self._flags[key]['VALUE_IS_STRUCT']:
            return dict
        return str

    @ensure_loaded
    def __setitem__(self, key, val):
        if not self._updating:
            raise RuntimeError("Metadata not opened for editing, use with block")
        if not isinstance(val, self._expected_type(key, val)):
            raise TypeError("Invalid type set for metadata {}".format(key))
        self._changed.add(key)
        self._records[key] = val

    @ensure_loaded
    def __delitem__(self, key):
        if not self._updating:
            raise RuntimeError("Metadata not opened for editing, use with block")
        del self._records[key]
        self._deleted.add(key)

    @property
    @ensure_loaded
    def pdfa_status(self):
        """Returns the PDF/A conformance level claimed by this PDF, or False

        A PDF may claim to PDF/A compliant without this being true. Use an
        independent verifier such as veraPDF to test if a PDF is truly
        conformant.

        Returns:
            str: The conformance level of the PDF/A, or an empty string if the
            PDF does not claim PDF/A conformance. Possible valid values
            are: 1A, 1B, 2A, 2B, 2U, 3A, 3B, 3U.
        """
        pdfaid = self._xmp.get_prefix_for_namespace(XMP_NS_PDFA_ID)
        key_part = pdfaid + 'part'
        key_conformance = pdfaid + 'conformance'
        try:
            return self._records[key_part] + self._records[key_conformance]
        except KeyError:
            return ''

    @property
    @ensure_loaded
    def pdfx_status(self):
        """Returns the PDF/X conformance level claimed by this PDF, or False

        A PDF may claim to PDF/X compliant without this being true. Use an
        independent verifier such as veraPDF to test if a PDF is truly
        conformant.

        Returns:
            str: The conformance level of the PDF/X, or an empty string if the
            PDF does not claim PDF/X conformance.
        """
        pdfxid = self._xmp.get_prefix_for_namespace(XMP_NS_PDFX_ID)
        pdfx_version = pdfxid + 'GTS_PDFXVersion'
        try:
            return self._records[pdfx_version]
        except KeyError:
            return ''
<|endoftext|>


<commit_before>from decimal import Decimal
from distutils.version import LooseVersion
from django.core.management.base import NoArgsCommand
from django.core import urlresolvers
from django.utils.importlib import import_module
import django
import imp
import logging
import re
import sys
import types

errors = []
logged_more = False
app_paths = {}
logging.getLogger().setLevel(logging.INFO)
logging.getLogger('satchmo_check').setLevel(logging.DEBUG)
log = logging.getLogger('satchmo_check')

class Command(NoArgsCommand):
    help = "Check the system to see if the Satchmo components are installed correctly."

    # These settings help to not import some dependencies before they are
    can_import_settings = False
    requires_model_validation = False

    def handle_noargs(self, **options):
        """Checks Satchmo installation and configuration.

        Tests, catches and shortly summarizes many common installation errors, without tracebacks.
        If was seen a traceback, it should be reported to a developer. (for now)
        Tracebacks are saved to the 'satchmo.log'. It helps to find cyclic dependencies etc.
        """
        from django.conf import settings
        global logged_more
        print_out("Checking your satchmo configuration.")
        try:
            import satchmo_store
        except ImportError:
            error_out("Satchmo is not installed correctly. Please verify satchmo is on your sys path.")
        print "Using Django version %s" % django.get_version()
        print "Using Satchmo version %s" % satchmo_store.get_version()
        #Check the Django version
        #Make sure we only get the X.Y.Z version info and not any other alpha or beta designations
        version_check = LooseVersion(".".join(map(str, django.VERSION)[:3]))
        if version_check < LooseVersion("1.2.3"):
            error_out("Django version must be >= 1.2.3")

        # Store these checked installation paths also to the paths overview
        verbose_check_install('satchmo', 'satchmo_store', verbose_name='Satchmo')

        verbose_check_install('django', 'django', '1.2.3')

        # Try importing all our dependencies
        verbose_check_install('', 'Crypto.Cipher', verbose_name='The Python Cryptography Toolkit')
        try:
            import Image
            verbose_check_install('Image', 'Image')
        except ImportError:
            verbose_check_install('PIL', 'PIL', verbose_name='The Python Imaging Library')

        verbose_check_install('reportlab', 'reportlab', '2.3')

        verbose_check_install('TRML2PDF', 'trml2pdf', '1.0', verbose_name='Tiny RML2PDF')

        verbose_check_install('django_registration', 'registration', '0.7')

        verbose_check_install('', 'yaml', verbose_name='YAML')

        verbose_check_install('sorl_thumbnail', 'sorl', '3.2.5', 'caf69b520632', 'Sorl imaging library')

        verbose_check_install('django_caching_app_plugins', 'app_plugins', '0.1.2', '53a31761e344')

        verbose_check_install('django_livesettings', 'livesettings', '1.4-8', 'e2769f9f60ec')

        verbose_check_install('django_signals_ahoy', 'signals_ahoy', '0.1.0', '9ad8779d4c63')

        verbose_check_install('django_threaded_multihost', 'threaded_multihost', '1.4.1', '7ca3743d8a70')

        verbose_check_install('django-keyedcache', 'keyedcache', '1.4-4', '4be18235b372')

        # Installers versions can be interesting for installation problems
        check_install('pip', 'pip')   # pip can not show the version number
        verbose_check_install('setuptools', 'setuptools', required=False)
        #verbose_check_install('mercurial', 'mercurial', required=False)

        try:
            cache_avail = settings.CACHE_BACKEND
        except AttributeError:
            error_out("A CACHE_BACKEND must be configured.")
        # Try looking up a url to see if there's a misconfiguration there
        try:
            url = urlresolvers.reverse('satchmo_search')
            # Catch SystemExit, because if an error occurs, `urlresolvers` usually calls sys.exit() and other error messages would be lost.
        except (Exception, SystemExit), e:
            error_out("Unable to resolve urls. Received error - %s" % formaterror(e))
        from l10n.l10n_settings import get_l10n_default_currency_symbol
        if not isinstance(get_l10n_default_currency_symbol(),types.UnicodeType):
            error_out("Your currency symbol should be a unicode string.")
        if 'satchmo_store.shop.SSLMiddleware.SSLRedirect' not in settings.MIDDLEWARE_CLASSES:
            error_out("You must have satchmo_store.shop.SSLMiddleware.SSLRedirect in your MIDDLEWARE_CLASSES.")
        if 'satchmo_store.shop.context_processors.settings' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
            error_out("You must have satchmo_store.shop.context_processors.settings in your TEMPLATE_CONTEXT_PROCESSORS.")
        if 'threaded_multihost.middleware.ThreadLocalMiddleware' not in settings.MIDDLEWARE_CLASSES:
            error_out("You must install django threaded multihost \n and place threaded_multihost.middleware.ThreadLocalMiddleware in your MIDDLEWARE_CLASSES.")
        if 'satchmo_store.accounts.email-auth.EmailBackend' not in settings.AUTHENTICATION_BACKENDS:
            error_out("You must have satchmo_store.accounts.email-auth.EmailBackend in your AUTHENTICATION_BACKENDS")
        if len(settings.SECRET_KEY) == 0:
            error_out("You must have SECRET_KEY set to a valid string in your settings.py file")
        python_ver = Decimal("%s.%s" % (sys.version_info[0], sys.version_info[1]))
        if python_ver < Decimal("2.4"):
            error_out("Python version must be at least 2.4.")
        if python_ver < Decimal("2.5"):
            try:
                from elementtree.ElementTree import Element
            except ImportError:
                error_out("Elementtree is not installed.")

        # Check all installed apps
        if not filter(lambda x: re.search('not .*(installed|imported)', x), errors):
            for appname in settings.INSTALLED_APPS:
                pkgtype, filename, root_filename = find_module_extend(appname)
                try:
                    app = import_module(appname)
                except (Exception, SystemExit):
                    if not pkgtype:
                        error_out('Can not find module "%s"' % appname)
                    else:
                        error_out('Can not import "%s"' % appname)
                        log.exception('Can not import "%s"' % appname)
                        logged_more = True
        else:
            log.debug('It does not test INSTALLED_APPS due to previous errors.')

        log.debug('\n'.join(2 * ['Installation paths:'] +
                    ['  %s : %s' % (k, sorted(list(v))) for k, v in sorted(app_paths.items())]
                ))
        apps_in_root = sorted(reduce(set.union,
                [v for k, v in app_paths.items() if k.startswith('/root')],
                set()))
        if apps_in_root:
            error_out('No package should be installed in the "/root" home directory, but packages %s are.' % (apps_in_root,))
            logged_more = True

        if len(errors) == 0:
            print_out("Your configuration has no errors.")
        else:
            print_out(""); print_out("The following errors were found:")
            for error in errors:
                print_out(error)
            if logged_more:
                print "Error details are in 'satchmo.log'"

def print_out(msg):
    "Print immediately to screen and to the log."
    log.info(msg)
    print msg

def error_out(msg):
    "Prints not to the log and at the end to the screen."
    log.error(msg)
    errors.append(msg)

def formaterror(e):
    "Format an exception like this: 'ExceptionName: error message'."
    exc_name = getattr(type(e), '__name__', None)
    if not exc_name:
        # some exceptions defined in C++ do not have an attribute __name__
        # e.g. backend.DatabaseError for postgres_psycopg2
        exc_name = re.sub(r"<.*'(.*)'>", '\\1', str(type(e)))
        exc_name = exc_name.split('.')[-1]
    return '%s: %s' % (exc_name, e)

def find_module_extend(appname):
    """Find module - support for "package.package.module".
    Returns tuple (pkgtype, filename, root_filename)   (result constants are defined in imp)
    root_filename is for the firstlevel package
    This does not find an yet imported module which is not on the python path now and was only at the time of import (typically store.localsite)
    """
    lpos = 0
    path = pkgtype = None
    try:
        while lpos < len(appname):
            rpos = (appname.find('.', lpos) < 0) and len(appname) or appname.find('.', lpos)
            dummyfile, filename, (suffix, mode, pkgtype) = imp.find_module(appname[lpos:rpos], path)
            path = [filename]
            if not lpos:
                root_filename = filename
            lpos = rpos + 1
        root_appname = appname.split('.')[0]
        if root_filename.endswith('/' + root_appname):
            root_filename = root_filename[:-len(root_appname) - 1]
        app_paths.setdefault(root_filename, set())
        app_paths[root_filename].add(root_appname)
        return (pkgtype, filename, root_filename)
    except ImportError:
        return (None, None, None)

APP_NOT_FOUND, APP_OLD_VERSION, APP_IMPORT_FAIL, APP_OK = range(4)

def check_install(project_name, appname, min_version=None, hg_hash=None):
    """Checks if package is installed, version is greater or equal to the required and can be imported.

    This uses different methods of determining the version for diffenent types of installation
    in this order: app.get_version() (custom), app.VERSION (tuple), app.__version__ (string), mercurial hash, setuptools version

      project_name  # verbose name for setuptools (can be with "-")
      package_name  # package name for python import
      min_version   # minimal required version (>=min_version)
      hg_hash       # hg hash of the commit, which should be in the repository. This should be consistent to min_version.
                      (A developper need not eventually to use specified important version (need not hg up tip) but should be informed to do pull.)
                      One met condition is sufficient.

    Returns tuple:  (result_code,     # any of APP_* constants
                     version_string)
    The import problem can be caused by a dependency on other packages and therefore it is differentiated from not installed.
    """
    # Version number can be obtained even without any requirements for a version this way: (all versions meet this)
    #    check_install(project_name, appname, min_version='.', hg_hash='')
    import os
    import time
    global logged_more
    isimported = False
    isversion = None
    version = ''
    # find it only
    pkgtype, filename, root_filename = find_module_extend(appname)
    isfound = (pkgtype != None)
    if isfound:
        try:
            # import it
            app = __import__(appname)
            isimported = True
        except ImportError:
            log.exception('Can not import "%s"' % appname)
            logged_more = True

    if isimported:
        try:
            # get version from app
            if hasattr(app, 'get_version'):
                get_version = app.get_version
                if callable(get_version):
                    version = get_version()
                else:
                    version = get_version
            elif hasattr(app, 'VERSION'):
                version = app.VERSION
            elif hasattr(app, '__version__'):
                version = app.__version__
            if isinstance(version, (list, tuple)):
                version = '.'.join(map(str, version))
            if version and version[0].isdigit() and min_version:
                isversion = LooseVersion(version) >= LooseVersion(min_version)
        except:
            pass

    if pkgtype == imp.PKG_DIRECTORY:   # and hg_hash != None and (version == None or not version[0].isdigit()):
        try:
            # get version from mercurial
            from mercurial import ui, hg
            try:
                linkpath = os.readlink(filename)
                dirname = os.path.join(os.path.dirname(filename), linkpath)
            except:
                dirname = filename
            repo = None
            hg_dir = os.path.normpath(os.path.join(dirname, '..'))
            repo = hg.repository(ui.ui(), hg_dir)
            try:
                node_id = repo.changelog.lookup(hg_hash)
                isversion = True
                #node = repo[node_id]    # required node
                node = repo['.']        # current node
                datestr = time.strftime('%Y-%m-%d', time.gmtime(node.date()[0]))
                version_hg = "hg-%s:%s %s" % (node.rev(), node.hex()[0:12], datestr)
                version = (version + ' ' + version_hg).strip()
            except:
                isversion = isversion or False
        except:
            pass

    if isfound and min_version and project_name and isversion == None:
        try:
            # get version from setuptools
            from pkg_resources import require
            version = require(project_name)[0].version
            isversion = isversion or False
            require('%s >=%s' % (project_name, min_version))
            isversion = True
        except:
            pass
    # If no required version specified, then it is also OK
    isversion = isversion or (isversion == None and min_version == None)
    result = isfound and (isversion and (isimported and APP_OK or APP_IMPORT_FAIL) or APP_OLD_VERSION) or APP_NOT_FOUND
    return (result, version)

def verbose_check_install(project_name, appname, min_version=None, hg_hash=None, verbose_name=None, required=True):
    """Check a pagkage and writes the results.

    Calls ``check_install`` (see for info about the similar parameters)
    verbose_name is used for messageses. Default is same as appname."""
    result, version = check_install(project_name, appname, min_version, hg_hash)
    verbose_name = (verbose_name or re.sub('[_-]', ' ', appname.capitalize()))
    if result != APP_NOT_FOUND and required or result == APP_OK:
        log.debug('%s: version %s ' % (verbose_name, version or '(unknown)'))
    #
    if result == APP_NOT_FOUND:
        msg = 'is not installed.'
    elif result == APP_OLD_VERSION:
        msg = 'should be upgraded to version %s or newer.' % min_version
    elif result == APP_IMPORT_FAIL:
        msg = 'can not be imported now, but the right version is probably installed. Maybe dependency problem.'
    elif result == APP_OK:
        msg = None
    #
    if msg and required:
        error_out(' '.join((verbose_name, msg)))
<commit_msg>Enhanced satchmo_check<commit_after>from decimal import Decimal
from distutils.version import LooseVersion
from django.core.management.base import NoArgsCommand
from django.core import urlresolvers
from django.utils.importlib import import_module
import django
import imp
import logging
import re
import sys
import types

errors = []
logged_more = False
app_paths = {}
logging.getLogger().setLevel(logging.INFO)
logging.getLogger('satchmo_check').setLevel(logging.DEBUG)
log = logging.getLogger('satchmo_check')

class Command(NoArgsCommand):
    help = "Check the system to see if the Satchmo components are installed correctly."

    # These settings help to not import some dependencies before they are
    can_import_settings = False
    requires_model_validation = False

    def handle_noargs(self, **options):
        """Checks Satchmo installation and configuration.

        Tests, catches and shortly summarizes many common installation errors, without tracebacks.
        If was seen a traceback, it should be reported to a developer. (for now)
        Tracebacks are saved to the 'satchmo.log'. It helps to find cyclic dependencies etc.
        """
        from django.conf import settings
        global logged_more
        print_out("Checking your satchmo configuration.")
        try:
            import satchmo_store
        except ImportError:
            error_out("Satchmo is not installed correctly. Please verify satchmo is on your sys path.")
        print "Using Django version %s" % django.get_version()
        print "Using Satchmo version %s" % satchmo_store.get_version()
        #Check the Django version
        #Make sure we only get the X.Y.Z version info and not any other alpha or beta designations
        version_check = LooseVersion(".".join(map(str, django.VERSION)[:3]))
        if version_check < LooseVersion("1.2.3"):
            error_out("Django version must be >= 1.2.3")

        # Store these checked installation paths also to the paths overview
        verbose_check_install('satchmo', 'satchmo_store', verbose_name='Satchmo')

        verbose_check_install('django', 'django', '1.2.3')

        # Try importing all our dependencies
        verbose_check_install('', 'Crypto.Cipher', verbose_name='The Python Cryptography Toolkit')
        try:
            import Image
            verbose_check_install('Image', 'Image')
        except ImportError:
            verbose_check_install('PIL', 'PIL', verbose_name='The Python Imaging Library')

        verbose_check_install('reportlab', 'reportlab', '2.3')

        verbose_check_install('TRML2PDF', 'trml2pdf', '1.0', verbose_name='Tiny RML2PDF')

        verbose_check_install('django_registration', 'registration', '0.7')

        verbose_check_install('', 'yaml', verbose_name='YAML')

        verbose_check_install('sorl_thumbnail', 'sorl', '3.2.5', 'caf69b520632', 'Sorl imaging library')

        verbose_check_install('django_caching_app_plugins', 'app_plugins', '0.1.2', '53a31761e344')

        verbose_check_install('django_livesettings', 'livesettings', '1.4-8', 'e2769f9f60ec')

        verbose_check_install('django_signals_ahoy', 'signals_ahoy', '0.1.0', '9ad8779d4c63')

        verbose_check_install('django_threaded_multihost', 'threaded_multihost', '1.4.1', '7ca3743d8a70')

        verbose_check_install('django-keyedcache', 'keyedcache', '1.4-4', '4be18235b372')

        # Installers versions can be interesting for installation problems
        check_install('pip', 'pip')   # pip can not show the version number
        verbose_check_install('setuptools', 'setuptools', required=False)
        #verbose_check_install('mercurial', 'mercurial', required=False)

        try:
            cache_avail = settings.CACHE_BACKEND
        except AttributeError:
            error_out("A CACHE_BACKEND must be configured.")
        # Try looking up a url to see if there's a misconfiguration there
        try:
            # The function urlresolvers.reverse has its own way of error reporting to screen and we have no access to it
            url = urlresolvers.reverse('satchmo_search')
            # Catch SystemExit, because if an error occurs, `urlresolvers` usually calls sys.exit() and other error messages would be lost.
        except (Exception, SystemExit), e:
            error_out("Unable to resolve urls. Received error - %s" % formaterror(e))
	try:
	    from l10n.l10n_settings import get_l10n_default_currency_symbol
	except:
	    pass
        if not isinstance(get_l10n_default_currency_symbol(),types.UnicodeType):
            error_out("Your currency symbol should be a unicode string.")
        if 'satchmo_store.shop.SSLMiddleware.SSLRedirect' not in settings.MIDDLEWARE_CLASSES:
            error_out("You must have satchmo_store.shop.SSLMiddleware.SSLRedirect in your MIDDLEWARE_CLASSES.")
        if 'satchmo_store.shop.context_processors.settings' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
            error_out("You must have satchmo_store.shop.context_processors.settings in your TEMPLATE_CONTEXT_PROCESSORS.")
        if 'threaded_multihost.middleware.ThreadLocalMiddleware' not in settings.MIDDLEWARE_CLASSES:
            error_out("You must install django threaded multihost \n and place threaded_multihost.middleware.ThreadLocalMiddleware in your MIDDLEWARE_CLASSES.")
        if 'satchmo_store.accounts.email-auth.EmailBackend' not in settings.AUTHENTICATION_BACKENDS:
            error_out("You must have satchmo_store.accounts.email-auth.EmailBackend in your AUTHENTICATION_BACKENDS")
        if len(settings.SECRET_KEY) == 0:
            error_out("You must have SECRET_KEY set to a valid string in your settings.py file")
        python_ver = Decimal("%s.%s" % (sys.version_info[0], sys.version_info[1]))
        if python_ver < Decimal("2.4"):
            error_out("Python version must be at least 2.4.")
        if python_ver < Decimal("2.5"):
            try:
                from elementtree.ElementTree import Element
            except ImportError:
                error_out("Elementtree is not installed.")

        # Check all installed apps
        if not filter(lambda x: re.search('not .*(installed|imported)', x), errors):
            for appname in settings.INSTALLED_APPS:
                pkgtype, filename, root_filename = find_module_extend(appname)
                try:
                    app = import_module(appname)
                except (Exception, SystemExit):
                    if not pkgtype:
                        error_out('Can not find module "%s"' % appname)
                    else:
                        error_out('Can not import "%s"' % appname)
                        log.exception('Can not import "%s"' % appname)
                        logged_more = True
        else:
            log.debug('It does not test INSTALLED_APPS due to previous errors.')

        log.debug('\n'.join(2 * ['Installation paths:'] +
                    ['  %s : %s' % (k, sorted(list(v))) for k, v in sorted(app_paths.items())]
                ))
        apps_in_root = sorted(reduce(set.union,
                [v for k, v in app_paths.items() if k.startswith('/root')],
                set()))
        if apps_in_root:
            error_out('No package should be installed in the "/root" home directory, but packages %s are.' % (apps_in_root,))
            logged_more = True

        if len(errors) == 0:
            print_out("Your configuration has no errors.")
        else:
            print_out(""); print_out("The following errors were found:")
            for error in errors:
                print_out(error)
            if logged_more:
                print "Error details are in 'satchmo.log'"

def print_out(msg):
    "Print immediately to screen and to the log."
    log.info(msg)
    print msg

def error_out(msg):
    "Prints not to the log and at the end to the screen."
    log.error(msg)
    errors.append(msg)

def formaterror(e):
    "Format an exception like this: 'ExceptionName: error message'."
    exc_name = getattr(type(e), '__name__', None)
    if not exc_name:
        # some exceptions defined in C++ do not have an attribute __name__
        # e.g. backend.DatabaseError for postgres_psycopg2
        exc_name = re.sub(r"<.*'(.*)'>", '\\1', str(type(e)))
        exc_name = exc_name.split('.')[-1]
    return '%s: %s' % (exc_name, e)

def find_module_extend(appname):
    """Find module - support for "package.package.module".
    Returns tuple (pkgtype, filename, root_filename)   (result constants are defined in imp)
    root_filename is for the firstlevel package
    This does not find an yet imported module which is not on the python path now and was only at the time of import (typically store.localsite)
    """
    lpos = 0
    path = pkgtype = None
    try:
        while lpos < len(appname):
            rpos = (appname.find('.', lpos) < 0) and len(appname) or appname.find('.', lpos)
            dummyfile, filename, (suffix, mode, pkgtype) = imp.find_module(appname[lpos:rpos], path)
            path = [filename]
            if not lpos:
                root_filename = filename
            lpos = rpos + 1
        root_appname = appname.split('.')[0]
        if root_filename.endswith('/' + root_appname):
            root_filename = root_filename[:-len(root_appname) - 1]
        app_paths.setdefault(root_filename, set())
        app_paths[root_filename].add(root_appname)
        return (pkgtype, filename, root_filename)
    except ImportError:
        return (None, None, None)

APP_NOT_FOUND, APP_OLD_VERSION, APP_IMPORT_FAIL, APP_OK = range(4)

def check_install(project_name, appname, min_version=None, hg_hash=None):
    """Checks if package is installed, version is greater or equal to the required and can be imported.

    This uses different methods of determining the version for diffenent types of installation
    in this order: app.get_version() (custom), app.VERSION (tuple), app.__version__ (string), mercurial hash, setuptools version

      project_name  # verbose name for setuptools (can be with "-")
      package_name  # package name for python import
      min_version   # minimal required version (>=min_version)
      hg_hash       # hg hash of the commit, which should be in the repository. This should be consistent to min_version.
                      (A developper need not eventually to use specified important version (need not hg up tip) but should be informed to do pull.)
                      One met condition is sufficient.

    Returns tuple:  (result_code,     # any of APP_* constants
                     version_string)
    The import problem can be caused by a dependency on other packages and therefore it is differentiated from not installed.
    """
    # Version number can be obtained even without any requirements for a version this way: (all versions meet this)
    #    check_install(project_name, appname, min_version='.', hg_hash='')
    import os
    import time
    global logged_more
    isimported = False
    isversion = None
    version = ''
    # find it only
    pkgtype, filename, root_filename = find_module_extend(appname)
    isfound = (pkgtype != None)
    if isfound:
        try:
            # import it
            app = __import__(appname)
            isimported = True
        except ImportError:
            log.exception('Can not import "%s"' % appname)
            logged_more = True

    if isimported:
        try:
            # get version from app
            if hasattr(app, 'get_version'):
                get_version = app.get_version
                if callable(get_version):
                    version = get_version()
                else:
                    version = get_version
            elif hasattr(app, 'VERSION'):
                version = app.VERSION
            elif hasattr(app, '__version__'):
                version = app.__version__
            if isinstance(version, (list, tuple)):
                version = '.'.join(map(str, version))
            if version and version[0].isdigit() and min_version:
                isversion = LooseVersion(version) >= LooseVersion(min_version)
        except:
            pass

    if pkgtype == imp.PKG_DIRECTORY:   # and hg_hash != None and (version == None or not version[0].isdigit()):
        try:
            # get version from mercurial
            from mercurial import ui, hg
            try:
                linkpath = os.readlink(filename)
                dirname = os.path.join(os.path.dirname(filename), linkpath)
            except:
                dirname = filename
            repo = None
            hg_dir = os.path.normpath(os.path.join(dirname, '..'))
            repo = hg.repository(ui.ui(), hg_dir)
            try:
                node_id = repo.changelog.lookup(hg_hash)
                isversion = True
                #node = repo[node_id]    # required node
                node = repo['.']        # current node
                datestr = time.strftime('%Y-%m-%d', time.gmtime(node.date()[0]))
                version_hg = "hg-%s:%s %s" % (node.rev(), node.hex()[0:12], datestr)
                version = (version + ' ' + version_hg).strip()
            except:
                isversion = isversion or False
        except:
            pass

    if isfound and min_version and project_name and isversion == None:
        try:
            # get version from setuptools
            from pkg_resources import require
            version = require(project_name)[0].version
            isversion = isversion or False
            require('%s >=%s' % (project_name, min_version))
            isversion = True
        except:
            pass
    # If no required version specified, then it is also OK
    isversion = isversion or (isversion == None and min_version == None)
    result = isfound and (isversion and (isimported and APP_OK or APP_IMPORT_FAIL) or APP_OLD_VERSION) or APP_NOT_FOUND
    return (result, version)

def verbose_check_install(project_name, appname, min_version=None, hg_hash=None, verbose_name=None, required=True):
    """Check a pagkage and writes the results.

    Calls ``check_install`` (see for info about the similar parameters)
    verbose_name is used for messageses. Default is same as appname."""
    result, version = check_install(project_name, appname, min_version, hg_hash)
    verbose_name = (verbose_name or re.sub('[_-]', ' ', appname.capitalize()))
    if result != APP_NOT_FOUND and required or result == APP_OK:
        log.debug('%s: version %s ' % (verbose_name, version or '(unknown)'))
    #
    if result == APP_NOT_FOUND:
        msg = 'is not installed.'
    elif result == APP_OLD_VERSION:
        msg = 'should be upgraded to version %s or newer.' % min_version
    elif result == APP_IMPORT_FAIL:
        msg = 'can not be imported now, but the right version is probably installed. Maybe dependency problem.'
    elif result == APP_OK:
        msg = None
    #
    if msg and required:
        error_out(' '.join((verbose_name, msg)))
<|endoftext|>