<commit_before>import re
from .specs import _get_default_specs
from enum import Enum

class AddressParts(Enum):
    """An enum containing all possible values for address parts"""
    name = 'N'
    organisation = 'O'
    street_address = 'A'
    dependent_locality = 'D'
    city = 'C'
    admin_area = 'S'
    postal_code = 'Z'
    sorting_code = 'X'

    # Country is special, since we made up this character ourselves.
    country = '0'

    @classmethod
    def significant(cls):
        """
        An ordered, reversed list of parts, so that the first item is the
        largest topological division. This also strips out postal and sorting
        code, since they're independent from all the others.
        """
        ordered_values = cls.__members__.values()
        reverse = reversed(list(ordered_values))
        return list(filter(lambda p: not p.name.endswith('_code'), reverse))

    @classmethod
    def from_string(cls, s):
        return cls.__members__[s]

class InvalidAddressException(ValueError):
    pass

class FieldValue(object):
    def __init__(self, value, details, subs):
        self.value = value
        self.details = details
        self.subs = subs

class FieldType(object):
    def __init__(self, key, label, required, options=None):
        self.key = key
        self.label = label
        self.required = required
        self.options = options

class Address(object):
    """An address value.

    The Address class can save and validate a full address, from any of the
    countries specified in the specifications passed to it. These
    specifications should be generated by the generate.download module, as they
    adhere to Google's i18n imported values.

    Mostly, you'll want to use the `get_data()`, `get_field_types()` and
    `set_field()` methods. These three methods should allow you to create a
    form that dynamically responds to any data and shows only relevant form
    fields. You can then simply call `str(address)` to get a fully valid
    address string, which you could use to send emails, for example.
    """

    def __init__(self, specs=_get_default_specs(), values=None):
        self.specs = specs
        self.defaults = FieldValue('', specs['details'], specs['subs'])

        self.fields = {}
        if values is not None:
            values_sorted = sorted(
                filter(lambda f: not f[0].name.endswith('_code'),
                       values.items()),
                key=lambda i: AddressParts.significant().index(i[0])
            )
            values_sorted += filter(lambda f: f[0].name.endswith('_code'),
                                    values.items())

            for field, value in values_sorted:
                self.set_field(field, value)

    def field_in_fmt(self, part):
        if part == AddressParts.country:
            return True

        data = self.get_specs()
        return '%{0}'.format(part.value) in data['fmt']

    def index_in_fmt(self, query_part):
        fmt = self.get_specs()['fmt']

        fmt_fields = []
        for substr in fmt.split('%')[1:]:
            c = substr[0]
            for part in AddressParts:
                if part.value == c:
                    fmt_fields.append(part)
                    break

        fmt_fields.append(AddressParts.country)

        return fmt_fields.index(query_part)

    def get_significant_fields(self):
        """
        Get a list of (key, value) objects of fields sorted by
        significance.
        """
        return sorted(
            filter(lambda f: not f[0].name.endswith('_code'),
                   self.fields.items()),
            key=lambda i: AddressParts.significant().index(i[0])
        )

    def get_specs(self):
        """Get an up-to-date list of specifications for the address.

        This returns a specs dict, based on the details of the filled in fields
        but falling back to the defaults from the specs. It takes the specs
        from the least significant field.
        """
        specs = self.defaults.details

        for field, value in self.get_significant_fields():
            specs.update(value.details)

        return specs

    def get_field_types(self):
        """Gets a list of currently known-about fields."""
        fields = []
        # add the country now, since it's always the first field to fill in
        fields.append(FieldType(
            key=AddressParts.country,
            label='country',
            required=True,
            options={
                key: value['details'].get('name', key)
                for key, value in self.defaults.subs.items()
                if '--' not in key
            }
        ))
        data = self.get_specs()

        if 'fmt' not in data:
            return fields

        # we want only parts we have, by significance
        sig = [part for part in AddressParts.significant()
               if self.field_in_fmt(part)]

        for depth, part in enumerate(sig):
            # already have this
            if part == AddressParts.country:
                continue

            # we want to know if the "parent field" has choices for this field
            # because if it does, but no option is chosen yet, we can'ts how
            # the other fields
            options = None
            parent = depth - 1
            parent_part = sig[parent]

            if fields[parent].options is not None and \
               parent_part not in self.fields:
                break

            # otherwise, if it is filled in, we only need to see if it has
            # options
            elif parent_part in self.fields:
                relevant = self.fields[parent_part]
                if len(relevant.subs) > 0:
                    options = {key: value['details'].get('name', key)
                               for key, value in relevant.subs.items()
                               if '--' not in key}

            if part == AddressParts.admin_area:
                label = relevant.details.get('state_name_type', 'province')
            else:
                label = part.name.replace('_', ' ')

            fields.append(FieldType(
                key=part,
                required=part.value in data['require'],
                label=label,
                options=options
            ))
        else:
            require = data['require']
            if self.field_in_fmt(AddressParts.postal_code):
                fields.append(FieldType(
                    key=AddressParts.postal_code,
                    required=AddressParts.postal_code.value in require,
                    label='postal code',
                    options=None
                ))
            if self.field_in_fmt(AddressParts.sorting_code):
                fields.append(FieldType(
                    key=AddressParts.sorting_code,
                    required=AddressParts.sorting_code.value in require,
                    label='sorting code',
                    options=None,
                ))

        # Sort the fields so that all fields with choices end up at the top,
        # and the other fields are sorted by their position in the output
        # format.
        sort = sorted(
            fields,
            key=lambda f: (-len(fields) + fields.index(f)
                           if f.options is not None
                           else self.index_in_fmt(f.key))
        )

        return sort

    def get_detail(self, field, prop):
        """
        Gets the value of a detail property for a certain field, but falls back
        to the defaults.
        """
        details = self.fields[field].details
        return details.get(prop, self.defaults.details.get(prop, None))

    def set_field(self, field, value):
        if isinstance(field, str):
            try:
                field = AddressParts.from_string(field)
            except KeyError:
                raise KeyError(
                    '{} is not a valid address part'.format(field)
                )

        chosen = {}

        sig = AddressParts.significant()
        try:
            depth = sig.index(field)
        except ValueError:
            depth = -1

        if depth == 0:
            relevant_part = sig[0]
            relevant = self.defaults
        elif depth > 0:
            relevant_part = sig[depth - 1]
            if relevant_part not in self.fields:
                relevant = False
            else:
                relevant = self.fields[relevant_part]

        if depth > -1 and relevant and \
           (depth == 0 or 'sub_keys' in relevant.details):
            if value not in relevant.subs:
                raise InvalidAddressException(
                    '"{0}" is not a valid value for field {1}'
                    .format(value, field.name)
                )

            chosen = relevant.subs[value]

        if not self.validate_field(field, value):
            raise InvalidAddressException(
                '"{0}" is not a valid value for field "{1}"'
                .format(value, field)
            )
        if AddressParts.postal_code == field:
            if not self.validate_postal_code(value):
                raise InvalidAddressException('Invalid postal code')

        fv = FieldValue(
            value=value,
            details=chosen.get('details', {}),
            subs=chosen.get('subs', {})
        )

        # Clear out all fields below this one if the old value had options
        if field in self.fields and len(self.fields[field].subs) > 0 and \
           depth > -1:
            parts = AddressParts.significant()
            parts += filter(lambda p: p.name.endswith('_code'), AddressParts)

            new_fields = {}
            index = parts.index(field)
            for f in self.fields:
                if parts.index(f) <= index:
                    new_fields[f] = self.fields[f]

            self.fields = new_fields

        self.fields[field] = fv

        if AddressParts.postal_code in self.fields:
            if not self.validate_postal_code():
                raise InvalidAddressException('Invalid postal code')

    def validate_postal_code(self, postal_code=None):
        if postal_code is None:
            if AddressParts.postal_code not in self.fields:
                return False

            postal_code = self.fields[AddressParts.postal_code].value

        regex = self.get_detail(AddressParts.country, 'zip')
        if not re.fullmatch(regex, postal_code):
            return False

        for value in self.fields.values():
            if 'zip' not in value.details:
                continue

            regex = value.details['zip']
            if not re.match(regex, postal_code):
                return False

        return True

    def validate_field(self, field, value):
        data = self.get_specs()

        return field.value not in data['require'] or \
            value is not None and value != ''

    def is_valid(self):
        data = self.get_specs()

        set_fields = ''.join(f.value for f in self.fields)
        fields_required = [r in set_fields for r in data['require']]

        fields_valid = [self.validate_field(part, value.value)
                        for part, value in self.fields.items()]
        code_valid = self.validate_postal_code()

        return all(fields_required) and all(fields_valid) and code_valid

    def __str__(self):
        data = self.get_specs()
        out = data['fmt']

        out = out.replace('%n', "\n")

        # replace each part by its value
        for part in AddressParts.__members__.values():
            # unless it doesn't exist
            if part not in self.fields:
                if part.value in data['require']:
                    raise KeyError('No value set for required field {0}'
                                   .format(part.name))

                out = out.replace('%{0}'.format(part.value), '')
                continue

            # possibly needs to be uppercase too
            val = self.fields[part].value
            if part.value in data['upper']:
                val = val.upper()

            out = out.replace('%{0}'.format(part.value), val)

        # append the country to the end
        out += "\n" + self.fields[AddressParts.country].details['name']

        # remove blank lines
        out = "\n".join(l.rstrip() for l in out.splitlines() if l.strip())

        return out
<commit_msg>Try to use ISOID as admin area key first<commit_after>import re
from .specs import _get_default_specs
from enum import Enum

class AddressParts(Enum):
    """An enum containing all possible values for address parts"""
    name = 'N'
    organisation = 'O'
    street_address = 'A'
    dependent_locality = 'D'
    city = 'C'
    admin_area = 'S'
    postal_code = 'Z'
    sorting_code = 'X'

    # Country is special, since we made up this character ourselves.
    country = '0'

    @classmethod
    def significant(cls):
        """
        An ordered, reversed list of parts, so that the first item is the
        largest topological division. This also strips out postal and sorting
        code, since they're independent from all the others.
        """
        ordered_values = cls.__members__.values()
        reverse = reversed(list(ordered_values))
        return list(filter(lambda p: not p.name.endswith('_code'), reverse))

    @classmethod
    def from_string(cls, s):
        return cls.__members__[s]

class InvalidAddressException(ValueError):
    pass

class FieldValue(object):
    def __init__(self, value, details, subs):
        self.value = value
        self.details = details
        self.subs = subs

class FieldType(object):
    def __init__(self, key, label, required, options=None):
        self.key = key
        self.label = label
        self.required = required
        self.options = options

class Address(object):
    """An address value.

    The Address class can save and validate a full address, from any of the
    countries specified in the specifications passed to it. These
    specifications should be generated by the generate.download module, as they
    adhere to Google's i18n imported values.

    Mostly, you'll want to use the `get_data()`, `get_field_types()` and
    `set_field()` methods. These three methods should allow you to create a
    form that dynamically responds to any data and shows only relevant form
    fields. You can then simply call `str(address)` to get a fully valid
    address string, which you could use to send emails, for example.
    """

    def __init__(self, specs=_get_default_specs(), values=None):
        self.specs = specs
        self.defaults = FieldValue('', specs['details'], specs['subs'])

        self.fields = {}
        if values is not None:
            values_sorted = sorted(
                filter(lambda f: not f[0].name.endswith('_code'),
                       values.items()),
                key=lambda i: AddressParts.significant().index(i[0])
            )
            values_sorted += filter(lambda f: f[0].name.endswith('_code'),
                                    values.items())

            for field, value in values_sorted:
                self.set_field(field, value)

    def field_in_fmt(self, part):
        if part == AddressParts.country:
            return True

        data = self.get_specs()
        return '%{0}'.format(part.value) in data['fmt']

    def index_in_fmt(self, query_part):
        fmt = self.get_specs()['fmt']

        fmt_fields = []
        for substr in fmt.split('%')[1:]:
            c = substr[0]
            for part in AddressParts:
                if part.value == c:
                    fmt_fields.append(part)
                    break

        fmt_fields.append(AddressParts.country)

        return fmt_fields.index(query_part)

    def get_significant_fields(self):
        """
        Get a list of (key, value) objects of fields sorted by
        significance.
        """
        return sorted(
            filter(lambda f: not f[0].name.endswith('_code'),
                   self.fields.items()),
            key=lambda i: AddressParts.significant().index(i[0])
        )

    def get_specs(self):
        """Get an up-to-date list of specifications for the address.

        This returns a specs dict, based on the details of the filled in fields
        but falling back to the defaults from the specs. It takes the specs
        from the least significant field.
        """
        specs = self.defaults.details

        for field, value in self.get_significant_fields():
            specs.update(value.details)

        return specs

    def get_field_types(self):
        """Gets a list of currently known-about fields."""
        fields = []
        # add the country now, since it's always the first field to fill in
        fields.append(FieldType(
            key=AddressParts.country,
            label='country',
            required=True,
            options={
                key: value['details'].get('name', key)
                for key, value in self.defaults.subs.items()
                if '--' not in key
            }
        ))
        data = self.get_specs()

        if 'fmt' not in data:
            return fields

        # we want only parts we have, by significance
        sig = [part for part in AddressParts.significant()
               if self.field_in_fmt(part)]

        for depth, part in enumerate(sig):
            # already have this
            if part == AddressParts.country:
                continue

            # we want to know if the "parent field" has choices for this field
            # because if it does, but no option is chosen yet, we can'ts how
            # the other fields
            options = None
            parent = depth - 1
            parent_part = sig[parent]

            if fields[parent].options is not None and \
               parent_part not in self.fields:
                break

            # otherwise, if it is filled in, we only need to see if it has
            # options
            elif parent_part in self.fields:
                relevant = self.fields[parent_part]
                if len(relevant.subs) > 0:
                    options = {
                        value['details'].get('isoid', key):
                        value['details'].get('name', key)
                        for key, value in relevant.subs.items()
                        if '--' not in key}

            if part == AddressParts.admin_area:
                label = relevant.details.get('state_name_type', 'province')
            else:
                label = part.name.replace('_', ' ')

            fields.append(FieldType(
                key=part,
                required=part.value in data['require'],
                label=label,
                options=options
            ))
        else:
            require = data['require']
            if self.field_in_fmt(AddressParts.postal_code):
                fields.append(FieldType(
                    key=AddressParts.postal_code,
                    required=AddressParts.postal_code.value in require,
                    label='postal code',
                    options=None
                ))
            if self.field_in_fmt(AddressParts.sorting_code):
                fields.append(FieldType(
                    key=AddressParts.sorting_code,
                    required=AddressParts.sorting_code.value in require,
                    label='sorting code',
                    options=None,
                ))

        # Sort the fields so that all fields with choices end up at the top,
        # and the other fields are sorted by their position in the output
        # format.
        sort = sorted(
            fields,
            key=lambda f: (-len(fields) + fields.index(f)
                           if f.options is not None
                           else self.index_in_fmt(f.key))
        )

        return sort

    def get_detail(self, field, prop):
        """
        Gets the value of a detail property for a certain field, but falls back
        to the defaults.
        """
        details = self.fields[field].details
        return details.get(prop, self.defaults.details.get(prop, None))

    def set_field(self, field, value):
        if isinstance(field, str):
            try:
                field = AddressParts.from_string(field)
            except KeyError:
                raise KeyError(
                    '{} is not a valid address part'.format(field)
                )

        chosen = {}

        sig = AddressParts.significant()
        try:
            depth = sig.index(field)
        except ValueError:
            depth = -1

        if depth == 0:
            relevant_part = sig[0]
            relevant = self.defaults
        elif depth > 0:
            relevant_part = sig[depth - 1]
            if relevant_part not in self.fields:
                relevant = False
            else:
                relevant = self.fields[relevant_part]

        if depth > -1 and relevant and \
           (depth == 0 or 'sub_keys' in relevant.details):
            if value not in relevant.subs:
                raise InvalidAddressException(
                    '"{0}" is not a valid value for field {1}'
                    .format(value, field.name)
                )

            chosen = relevant.subs[value]

        if not self.validate_field(field, value):
            raise InvalidAddressException(
                '"{0}" is not a valid value for field "{1}"'
                .format(value, field)
            )
        if AddressParts.postal_code == field:
            if not self.validate_postal_code(value):
                raise InvalidAddressException('Invalid postal code')

        fv = FieldValue(
            value=value,
            details=chosen.get('details', {}),
            subs=chosen.get('subs', {})
        )

        # Clear out all fields below this one if the old value had options
        if field in self.fields and len(self.fields[field].subs) > 0 and \
           depth > -1:
            parts = AddressParts.significant()
            parts += filter(lambda p: p.name.endswith('_code'), AddressParts)

            new_fields = {}
            index = parts.index(field)
            for f in self.fields:
                if parts.index(f) <= index:
                    new_fields[f] = self.fields[f]

            self.fields = new_fields

        self.fields[field] = fv

        if AddressParts.postal_code in self.fields:
            if not self.validate_postal_code():
                raise InvalidAddressException('Invalid postal code')

    def validate_postal_code(self, postal_code=None):
        if postal_code is None:
            if AddressParts.postal_code not in self.fields:
                return False

            postal_code = self.fields[AddressParts.postal_code].value

        regex = self.get_detail(AddressParts.country, 'zip')
        if not re.fullmatch(regex, postal_code):
            return False

        for value in self.fields.values():
            if 'zip' not in value.details:
                continue

            regex = value.details['zip']
            if not re.match(regex, postal_code):
                return False

        return True

    def validate_field(self, field, value):
        data = self.get_specs()

        return field.value not in data['require'] or \
            value is not None and value != ''

    def is_valid(self):
        data = self.get_specs()

        set_fields = ''.join(f.value for f in self.fields)
        fields_required = [r in set_fields for r in data['require']]

        fields_valid = [self.validate_field(part, value.value)
                        for part, value in self.fields.items()]
        code_valid = self.validate_postal_code()

        return all(fields_required) and all(fields_valid) and code_valid

    def __str__(self):
        data = self.get_specs()
        out = data['fmt']

        out = out.replace('%n', "\n")

        # replace each part by its value
        for part in AddressParts.__members__.values():
            # unless it doesn't exist
            if part not in self.fields:
                if part.value in data['require']:
                    raise KeyError('No value set for required field {0}'
                                   .format(part.name))

                out = out.replace('%{0}'.format(part.value), '')
                continue

            # possibly needs to be uppercase too
            val = self.fields[part].value
            if part.value in data['upper']:
                val = val.upper()

            out = out.replace('%{0}'.format(part.value), val)

        # append the country to the end
        out += "\n" + self.fields[AddressParts.country].details['name']

        # remove blank lines
        out = "\n".join(l.rstrip() for l in out.splitlines() if l.strip())

        return out
<|endoftext|>


<commit_before>#
# Copyright (c) SAS Institute Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""
rBuild-specific errors.
"""
import os
from conary.lib import util

# make ParseError available from here as well
# pylint: disable-msg=W0611
from conary.errors import ParseError

class RbuildBaseError(RuntimeError):
    """
    B{C{RbuildBaseError}} - Base class for all rBuild exceptions.

    New exceptions should derive from a subclass of this one, for
    example C{RbuildPluginError}.

    @cvar template: A template string used when displaying the
                    exception. Must use only keyword substitution.
    @cvar params: A list of parameter names used in the above template.
    """
    template = 'An unknown error has occurred'
    params = []

    def __init__(self, *args, **kwargs):
        RuntimeError.__init__(self)
        name = self.__class__.__name__

        # Sanity check
        for param in self.params:
            assert not hasattr(self.__class__, param), ("Parameter %r "
                    "conflicts with class dictionary, name it "
                    "something else." % param)
            assert param not in self.__dict__, ("Parameter %r "
                    "conflicts with instance dictionary, name it "
                    "something else." % param)

        self._values = dict()
        if kwargs:
            # Use keyword arguments
            if args:
                raise TypeError("Exception %s cannot take both positional "
                    "and keyword arguments" % name)

            missing = set(self.params) - set(kwargs)
            if missing:
                missingStr = ", ".join("%r" % x for x in missing)
                raise TypeError("Expected argument%s %s to exception %s" % (
                    len(missing) != 1 and 's' or '', missingStr, name))

            for key, value in kwargs.iteritems():
                if key not in self.params:
                    raise TypeError("Exception %s got an unexpected "
                        "argument %r" % (name, key))
                self._values[key] = value
        else:
            # Use positional arguments
            if len(self.params) != len(args):
                raise TypeError("Exception %s takes exactly %d argument%s "
                    "(%d given)" % (name, len(self.params),
                    len(self.params) != 1 and "s" or "", len(args)))

            for name, value in zip(self.params, args):
                self._values[name] = value

    def __getattr__(self, name):
        if name in self._values:
            return self._values[name]
        raise AttributeError(name)

    def __str__(self):
        return self.template % self._values

    def __repr__(self):
        params = ', '.join('%s=%r' % (x, self._values[x])
                for x in self.params)
        return '%s(%s)' % (self.__class__.__name__, params)

class RbuildInternalError(RbuildBaseError):
    """
    B{C{RbuildInternalError}} - superclass for all errors that are not meant to be
    seen.

    Errors deriving from InternalError should never occur, but when they do
    they indicate a failure within the code to handle some unexpected case.

    Do not raise this exception directly.
    """

class RbuildError(RbuildBaseError):
    """
    B{C{RbuildError}} - Internal rBuild errors

    This error may be raised only by rBuild internals, not by plugins.
    It may be raised directly but creating a new subclass for each
    specific case is recommended.
    """
    template = "%(msg)s"
    params = ['msg']

class PluginError(RbuildBaseError):
    """
    B{C{RbuildPluginError}} - rBuild plugin errors

    This error may be raised only by rBuild plugins, not by rBuild
    internals, and is the superclass for more specific errors raised
    by rBuild plugins. It may be raised directly but creating a new
    subclass for each specific case is recommended.
    """
    template = "%(msg)s"
    params = ['msg']

#Uncomment this, possibly modifying it, when we add deprecation
#infrastructure -- https://issues.rpath.com/browse/RBLD-147
#class DeprecatedInterfaceError(RbuildBaseError):
#    """
#    B{C{DeprecatedInterfaceError}} - Interface deprecated
#
#    This interface has been deprecated and may be removed in the
#    specified version.
#    """
#    Unsupportedtemplate = ('Deprecated interface %(name)s:'
#                           ' will be removed in rBuild %(version)s')
#    params = ['name', 'version' ]

class IncompleteInterfaceError(RbuildBaseError):
    """
    B{C{IncompleteInterfaceError}} - Interface unavailable in this configuration

    This error is raised by stub functions that are not filled in;
    for example, a function that makes sense only in the context
    of filesystem operations might raise this error if called in
    the context of repository operations.
    """
    template = "Unsupported interface: %(msg)s"
    params = ['msg']

class BadParameterError(RbuildBaseError):
    """
    Raised when a command is given bad parameters at the command line.
    """
    template = "%(msg)s"
    params = ['msg']

## BEGIN Internal Errors
class InvalidAPIMethodError(RbuildInternalError):
    "Raised when an unknown API method is referenced by an internal call."
    template = "No such API method %(method)r"
    params = ['method']

class InvalidHookReturnError(RbuildInternalError):
    "Raised when a prehook returns something unexpected."
    template = ("Invalid return value from prehook %(hook)r "
            "for function %(method)r")
    params = ['hook', 'method']

class MissingPluginError(RbuildInternalError):
    """
    Raised on attempts to access a plugin that is not known to the
    C{RbuildHandle} object.
    """
    template = "Plugin %(pluginName)r is not loaded"
    params = ['pluginName']

class InternalRmakeFacadeError(RbuildError):
    """
    Raised when rMake returns results that rBuild does not know how
    to interpret.
    """
    template = "Unexpected results from rMake: %(msg)r"
    params = ['msg']

## END Internal Errors

## BEGIN rBuild Errors
class MissingProductStoreError(RbuildError):
    template = "Directory %(path)r does not contain a product checkout"
    params = ['path']

class MissingActiveStageError(RbuildError):
    template = "Directory %(path)r is not a valid stage"
    params = ["path"]

class MissingGroupSearchPathElementError(RbuildError):
    template = ("Group search path element "
            "%(name)s=%(version)s[%(flavor)s] was not found")
    params = ['name', 'version', 'flavor']

class MissingImageDefinitionError(RbuildError):
    template = '\n'.join((
        'Product Definition %(name)s contains no image definitions',
        'Please add at least one image to your product definition'))
    params = ['name']

class RbuilderError(RbuildError):
    template = "rBuilder error %(error)s: %(frozen)r"
    params = ['error', 'frozen']

class RbuilderUserError(RbuilderError):
    template = 'Error retrieving user details: %(error)s: %(frozen)r'

## END rBuild Errors

## BEGIN Plugin Errors
class ConfigParseError(PluginError):
    """
    Raised when there is a problem loading a descriptor configuration file
    """
    template = "Error parsing '%(file)s' at %(location)s"
    params = ['file', 'location']

class UnauthorizedActionError(PluginError):
    """
    Raised when the user performs an action for which they are not authorized
    """
    template = "You are not authorized to %(action)s"
    params = ['action']

## END Plugin Errors

#: error that is output when a Python exception makes it to the command
#: line.
_ERROR_MESSAGE = '''
ERROR: An unexpected condition has occurred in rBuild.  This is
most likely due to insufficient handling of erroneous input, but
may be some other bug.  In either case, please report the error at
https://issues.rpath.com/ and attach to the issue the file
%(stackfile)s

To get a debug prompt, rerun the command with the --debug-all argument.

For more information, go to:
http://wiki.rpath.com/wiki/Conary:How_To_File_An_Effective_Bug_Report
For more debugging help, please go to #conary on freenode.net
or email conary-list@lists.rpath.com.

Error details follow:

%(filename)s:%(lineno)s
%(errtype)s: %(errmsg)s

The complete related traceback has been saved as %(stackfile)s
'''

def _findCheckoutRoot():
    """
    Find the top-level directory of the current checkout, if any.
    @return: directory name, or None if no checkout found
    """
    dirName = os.getcwd()
    for _ in range(dirName.count(os.path.sep)+1):
        if os.path.isdir(os.path.join(dirName, '.rbuild')):
            return dirName
        dirName = os.path.dirname(dirName)
    # if not in a checkout, default to $HOME if it exists
    # depends on getenv returning None for keys that do not exist
    return os.getenv('HOME')

def genExcepthook(*args, **kw):
    #pylint: disable-msg=C0999
    # just passes arguments through
    """
    Generates an exception handling hook that brings up a debugger.

    If the current working directory is underneath a product checkout,
    a full traceback will be output in
    C{<checkout root>/.rbuild/tracebacks/}, otherwise one will be
    output in C{/tmp}.

    Example::
        sys.excepthook = genExceptHook(debugAll=True)
    """

    #pylint: disable-msg=C0103
    # follow external convention
    def excepthook(e_type, e_value, e_traceback):
        """Exception hook wrapper"""
        checkoutRoot = _findCheckoutRoot()
        outputDir = None
        if checkoutRoot:
            #pylint: disable-msg=W0702
            # No exception type(s) specified - a generic handler is
            # warranted as this is in an exception handler.
            try:
                outputDir = checkoutRoot + '/.rbuild/tracebacks'
                if not os.path.exists(outputDir):
                    os.mkdir(outputDir, 0700)
                elif not os.access(outputDir, os.W_OK):
                    outputDir = None
            except:
                # fall back gracefully if we can't create the directory
                outputDir = None

        if outputDir:
            baseHook = util.genExcepthook(error=_ERROR_MESSAGE,
                prefix=outputDir + '/rbuild-error-', *args, **kw)
        else:
            baseHook = util.genExcepthook(error=_ERROR_MESSAGE,
                prefix='rbuild-error-', *args, **kw)

        baseHook(e_type, e_value, e_traceback)

    return excepthook

#pylint: disable-msg=C0103
# this shouldn't be upper case.
_uncatchableExceptions = (KeyboardInterrupt, SystemExit)
<commit_msg>Updated error message with up-to-date URLs<commit_after>#
# Copyright (c) SAS Institute Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""
rBuild-specific errors.
"""
import os
from conary.lib import util

# make ParseError available from here as well
# pylint: disable-msg=W0611
from conary.errors import ParseError

class RbuildBaseError(RuntimeError):
    """
    B{C{RbuildBaseError}} - Base class for all rBuild exceptions.

    New exceptions should derive from a subclass of this one, for
    example C{RbuildPluginError}.

    @cvar template: A template string used when displaying the
                    exception. Must use only keyword substitution.
    @cvar params: A list of parameter names used in the above template.
    """
    template = 'An unknown error has occurred'
    params = []

    def __init__(self, *args, **kwargs):
        RuntimeError.__init__(self)
        name = self.__class__.__name__

        # Sanity check
        for param in self.params:
            assert not hasattr(self.__class__, param), ("Parameter %r "
                    "conflicts with class dictionary, name it "
                    "something else." % param)
            assert param not in self.__dict__, ("Parameter %r "
                    "conflicts with instance dictionary, name it "
                    "something else." % param)

        self._values = dict()
        if kwargs:
            # Use keyword arguments
            if args:
                raise TypeError("Exception %s cannot take both positional "
                    "and keyword arguments" % name)

            missing = set(self.params) - set(kwargs)
            if missing:
                missingStr = ", ".join("%r" % x for x in missing)
                raise TypeError("Expected argument%s %s to exception %s" % (
                    len(missing) != 1 and 's' or '', missingStr, name))

            for key, value in kwargs.iteritems():
                if key not in self.params:
                    raise TypeError("Exception %s got an unexpected "
                        "argument %r" % (name, key))
                self._values[key] = value
        else:
            # Use positional arguments
            if len(self.params) != len(args):
                raise TypeError("Exception %s takes exactly %d argument%s "
                    "(%d given)" % (name, len(self.params),
                    len(self.params) != 1 and "s" or "", len(args)))

            for name, value in zip(self.params, args):
                self._values[name] = value

    def __getattr__(self, name):
        if name in self._values:
            return self._values[name]
        raise AttributeError(name)

    def __str__(self):
        return self.template % self._values

    def __repr__(self):
        params = ', '.join('%s=%r' % (x, self._values[x])
                for x in self.params)
        return '%s(%s)' % (self.__class__.__name__, params)

class RbuildInternalError(RbuildBaseError):
    """
    B{C{RbuildInternalError}} - superclass for all errors that are not meant to be
    seen.

    Errors deriving from InternalError should never occur, but when they do
    they indicate a failure within the code to handle some unexpected case.

    Do not raise this exception directly.
    """

class RbuildError(RbuildBaseError):
    """
    B{C{RbuildError}} - Internal rBuild errors

    This error may be raised only by rBuild internals, not by plugins.
    It may be raised directly but creating a new subclass for each
    specific case is recommended.
    """
    template = "%(msg)s"
    params = ['msg']

class PluginError(RbuildBaseError):
    """
    B{C{RbuildPluginError}} - rBuild plugin errors

    This error may be raised only by rBuild plugins, not by rBuild
    internals, and is the superclass for more specific errors raised
    by rBuild plugins. It may be raised directly but creating a new
    subclass for each specific case is recommended.
    """
    template = "%(msg)s"
    params = ['msg']

#Uncomment this, possibly modifying it, when we add deprecation
#infrastructure -- https://issues.rpath.com/browse/RBLD-147
#class DeprecatedInterfaceError(RbuildBaseError):
#    """
#    B{C{DeprecatedInterfaceError}} - Interface deprecated
#
#    This interface has been deprecated and may be removed in the
#    specified version.
#    """
#    Unsupportedtemplate = ('Deprecated interface %(name)s:'
#                           ' will be removed in rBuild %(version)s')
#    params = ['name', 'version' ]

class IncompleteInterfaceError(RbuildBaseError):
    """
    B{C{IncompleteInterfaceError}} - Interface unavailable in this configuration

    This error is raised by stub functions that are not filled in;
    for example, a function that makes sense only in the context
    of filesystem operations might raise this error if called in
    the context of repository operations.
    """
    template = "Unsupported interface: %(msg)s"
    params = ['msg']

class BadParameterError(RbuildBaseError):
    """
    Raised when a command is given bad parameters at the command line.
    """
    template = "%(msg)s"
    params = ['msg']

## BEGIN Internal Errors
class InvalidAPIMethodError(RbuildInternalError):
    "Raised when an unknown API method is referenced by an internal call."
    template = "No such API method %(method)r"
    params = ['method']

class InvalidHookReturnError(RbuildInternalError):
    "Raised when a prehook returns something unexpected."
    template = ("Invalid return value from prehook %(hook)r "
            "for function %(method)r")
    params = ['hook', 'method']

class MissingPluginError(RbuildInternalError):
    """
    Raised on attempts to access a plugin that is not known to the
    C{RbuildHandle} object.
    """
    template = "Plugin %(pluginName)r is not loaded"
    params = ['pluginName']

class InternalRmakeFacadeError(RbuildError):
    """
    Raised when rMake returns results that rBuild does not know how
    to interpret.
    """
    template = "Unexpected results from rMake: %(msg)r"
    params = ['msg']

## END Internal Errors

## BEGIN rBuild Errors
class MissingProductStoreError(RbuildError):
    template = "Directory %(path)r does not contain a product checkout"
    params = ['path']

class MissingActiveStageError(RbuildError):
    template = "Directory %(path)r is not a valid stage"
    params = ["path"]

class MissingGroupSearchPathElementError(RbuildError):
    template = ("Group search path element "
            "%(name)s=%(version)s[%(flavor)s] was not found")
    params = ['name', 'version', 'flavor']

class MissingImageDefinitionError(RbuildError):
    template = '\n'.join((
        'Product Definition %(name)s contains no image definitions',
        'Please add at least one image to your product definition'))
    params = ['name']

class RbuilderError(RbuildError):
    template = "rBuilder error %(error)s: %(frozen)r"
    params = ['error', 'frozen']

class RbuilderUserError(RbuilderError):
    template = 'Error retrieving user details: %(error)s: %(frozen)r'

## END rBuild Errors

## BEGIN Plugin Errors
class ConfigParseError(PluginError):
    """
    Raised when there is a problem loading a descriptor configuration file
    """
    template = "Error parsing '%(file)s' at %(location)s"
    params = ['file', 'location']

class UnauthorizedActionError(PluginError):
    """
    Raised when the user performs an action for which they are not authorized
    """
    template = "You are not authorized to %(action)s"
    params = ['action']

## END Plugin Errors

#: error that is output when a Python exception makes it to the command
#: line.
_ERROR_MESSAGE = '''
ERROR: An unexpected condition has occurred in rBuild.  This is
most likely due to insufficient handling of erroneous input, but
may be some other bug.  In either case, please report the error at
http://opensource.sas.com/its/ and attach to the issue the file
%(stackfile)s

To get a debug prompt, rerun the command with the --debug-all argument.

For more information, go to:
https://opensource.sas.com/conarywiki/index.php/Conary:How_To_File_An_Effective_Bug_Report

Error details follow:

%(filename)s:%(lineno)s
%(errtype)s: %(errmsg)s

The complete related traceback has been saved as %(stackfile)s
'''

def _findCheckoutRoot():
    """
    Find the top-level directory of the current checkout, if any.
    @return: directory name, or None if no checkout found
    """
    dirName = os.getcwd()
    for _ in range(dirName.count(os.path.sep)+1):
        if os.path.isdir(os.path.join(dirName, '.rbuild')):
            return dirName
        dirName = os.path.dirname(dirName)
    # if not in a checkout, default to $HOME if it exists
    # depends on getenv returning None for keys that do not exist
    return os.getenv('HOME')

def genExcepthook(*args, **kw):
    #pylint: disable-msg=C0999
    # just passes arguments through
    """
    Generates an exception handling hook that brings up a debugger.

    If the current working directory is underneath a product checkout,
    a full traceback will be output in
    C{<checkout root>/.rbuild/tracebacks/}, otherwise one will be
    output in C{/tmp}.

    Example::
        sys.excepthook = genExceptHook(debugAll=True)
    """

    #pylint: disable-msg=C0103
    # follow external convention
    def excepthook(e_type, e_value, e_traceback):
        """Exception hook wrapper"""
        checkoutRoot = _findCheckoutRoot()
        outputDir = None
        if checkoutRoot:
            #pylint: disable-msg=W0702
            # No exception type(s) specified - a generic handler is
            # warranted as this is in an exception handler.
            try:
                outputDir = checkoutRoot + '/.rbuild/tracebacks'
                if not os.path.exists(outputDir):
                    os.mkdir(outputDir, 0700)
                elif not os.access(outputDir, os.W_OK):
                    outputDir = None
            except:
                # fall back gracefully if we can't create the directory
                outputDir = None

        if outputDir:
            baseHook = util.genExcepthook(error=_ERROR_MESSAGE,
                prefix=outputDir + '/rbuild-error-', *args, **kw)
        else:
            baseHook = util.genExcepthook(error=_ERROR_MESSAGE,
                prefix='rbuild-error-', *args, **kw)

        baseHook(e_type, e_value, e_traceback)

    return excepthook

#pylint: disable-msg=C0103
# this shouldn't be upper case.
_uncatchableExceptions = (KeyboardInterrupt, SystemExit)
<|endoftext|>


<commit_before>
__doc__ = """
Dirt Simple Events

A Dispatcher (or a subclass of Dispatcher) stores event handlers that
are 'fired' simple event objects when interesting things happen.

Create a dispatcher:

  >>> d = Dispatcher()

Now create a handler for the event and subscribe it to the dispatcher
to handle Event events.  A handler is a simple function or method that
accepts the event as an argument:

  >>> def handler1(event): print(repr(event))
  >>> d.subscribe(Event, handler1)

Now dispatch a new event into the dispatcher, and see handler1 get
fired:

  >>> d.dispatch(Event(foo='bar', data='yours', used_by='the event handlers'))
  <rdflib.events.Event ['data', 'foo', 'used_by']>
"""

__all__ = ['Event', 'Dispatcher']

class Event(object):
    """
    An event is a container for attributes.  The source of an event
    creates this object, or a subclass, gives it any kind of data that
    the events handlers need to handle the event, and then calls
    notify(event).

    The target of an event registers a function to handle the event it
    is interested with subscribe().  When a sources calls
    notify(event), each subscriber to that even will be called i no
    particular order.
    """

    def __init__(self, **kw):
        self.__dict__.update(kw)

    def __repr__(self):
        attrs = self.__dict__.keys()
        attrs.sort()
        return '<rdflib.events.Event %s>' % ([a for a in attrs],)

class Dispatcher(object):
    """
    An object that can dispatch events to a privately managed group of
    subscribers.
    """

    _dispatch_map = None

    def set_map(self, amap):
        self._dispatch_map = amap

    def get_map(self):
        return self._dispatch_map

    def subscribe(self, event_type, handler):
        """ Subscribe the given handler to an event_type.  Handlers
        are called in the order they are subscribed.
        """
        if self._dispatch_map is None:
            self.set_map({})
        lst = self._dispatch_map.get(event_type, None)
        if lst is None:
            lst = [handler]
        else:
            lst.append(handler)
        self._dispatch_map[event_type] = lst

    def dispatch(self, event):
        """ Dispatch the given event to the subscribed handlers for
        the event's type"""
        if self._dispatch_map is not None:
            lst = self._dispatch_map.get(type(event), None)
            if lst is None:
                raise ValueError("unknown event type: %s" % type(event))
            for l in lst:
                l(event)

def test():
    import doctest
    doctest.testmod()

if __name__ == '__main__':
    test()
<commit_msg>small doc typo<commit_after>__doc__ = """
Dirt Simple Events

A Dispatcher (or a subclass of Dispatcher) stores event handlers that
are 'fired' simple event objects when interesting things happen.

Create a dispatcher:

  >>> d = Dispatcher()

Now create a handler for the event and subscribe it to the dispatcher
to handle Event events.  A handler is a simple function or method that
accepts the event as an argument:

  >>> def handler1(event): print(repr(event))
  >>> d.subscribe(Event, handler1)

Now dispatch a new event into the dispatcher, and see handler1 get
fired:

  >>> d.dispatch(Event(foo='bar', data='yours', used_by='the event handlers'))
  <rdflib.events.Event ['data', 'foo', 'used_by']>
"""

__all__ = ['Event', 'Dispatcher']

class Event(object):
    """
    An event is a container for attributes.  The source of an event
    creates this object, or a subclass, gives it any kind of data that
    the events handlers need to handle the event, and then calls
    notify(event).

    The target of an event registers a function to handle the event it
    is interested with subscribe().  When a sources calls
    notify(event), each subscriber to that event will be called in no
    particular order.
    """

    def __init__(self, **kw):
        self.__dict__.update(kw)

    def __repr__(self):
        attrs = self.__dict__.keys()
        attrs.sort()
        return '<rdflib.events.Event %s>' % ([a for a in attrs],)

class Dispatcher(object):
    """
    An object that can dispatch events to a privately managed group of
    subscribers.
    """

    _dispatch_map = None

    def set_map(self, amap):
        self._dispatch_map = amap

    def get_map(self):
        return self._dispatch_map

    def subscribe(self, event_type, handler):
        """ Subscribe the given handler to an event_type.  Handlers
        are called in the order they are subscribed.
        """
        if self._dispatch_map is None:
            self.set_map({})
        lst = self._dispatch_map.get(event_type, None)
        if lst is None:
            lst = [handler]
        else:
            lst.append(handler)
        self._dispatch_map[event_type] = lst

    def dispatch(self, event):
        """ Dispatch the given event to the subscribed handlers for
        the event's type"""
        if self._dispatch_map is not None:
            lst = self._dispatch_map.get(type(event), None)
            if lst is None:
                raise ValueError("unknown event type: %s" % type(event))
            for l in lst:
                l(event)

def test():
    import doctest
    doctest.testmod()

if __name__ == '__main__':
    test()
<|endoftext|>


<commit_before>from rdflib.backends import Backend
from rdflib.syntax import serializer, serializers
from rdflib.syntax import parsers

_kinds = {}
_adaptors = {}

def register(name, kind, module_path, class_name):
    _module_info = _kinds.get(kind, None)
    if _module_info is None:
        _module_info = _kinds[kind] = {}
    _module_info[name] = (module_path, class_name)

def get(name, kind):
    _module_info = _kinds.get(kind)
    if _module_info and name in _module_info:
        module_path, class_name = _module_info[name]
        module = __import__(module_path, globals(), locals(), True)
        return getattr(module, class_name)
    else:
        Adaptor = kind # TODO: look up of adaptor, for now just use kind
        Adaptee = get(name, _adaptors[kind])
        def const(*args, **keywords):
            return Adaptor(Adaptee(*args, **keywords))
        return const

def register_adaptor(adaptor, adaptee):
    _adaptors[adaptor] = adaptee

register_adaptor(serializer.Serializer, serializers.Serializer)
#register_adaptor(parser.Parser, parsers.Parser)

register('xml', serializers.Serializer,
         'rdflib.syntax.serializers.XMLSerializer', 'XMLSerializer')

register('pretty-xml', serializers.Serializer,
         'rdflib.syntax.serializers.PrettyXMLSerializer', 'PrettyXMLSerializer')

register('nt', serializers.Serializer,
         'rdflib.syntax.serializers.NTSerializer', 'NTSerializer')

register('xml', parsers.Parser,
         'rdflib.syntax.parsers.RDFXMLParser', 'RDFXMLParser')

register('n3', parsers.Parser,
         'rdflib.syntax.parsers.N3Parser', 'N3Parser')

register('nt', parsers.Parser,
         'rdflib.syntax.parsers.NTParser', 'NTParser')

register('default', Backend,
         'rdflib.backends.IOMemory', 'IOMemory')

register('IOMemory', Backend,
         'rdflib.backends.IOMemory', 'IOMemory')

register('Memory', Backend,
         'rdflib.backends.Memory', 'Memory')

register('Sleepycat', Backend,
         'rdflib.backends.Sleepycat', 'Sleepycat')

register('Sleepycat_new', Backend,
         'rdflib.backends.Sleepycat_new', 'Sleepycat_new')

register('ZODB', Backend,
         'rdflib.backends.ZODB', 'ZODB')

register('sqlobject', Backend,
         'rdflib.backends._sqlobject', 'SQLObject')

register('Redland', Backend,
         'rdflib.backends.Redland', 'Redland')
<commit_msg>tried to improve exception when plugin is not found<commit_after>from rdflib.backends import Backend
from rdflib.syntax import serializer, serializers
from rdflib.syntax import parsers

_kinds = {}
_adaptors = {}

def register(name, kind, module_path, class_name):
    _module_info = _kinds.get(kind, None)
    if _module_info is None:
        _module_info = _kinds[kind] = {}
    _module_info[name] = (module_path, class_name)

def get(name, kind):
    _module_info = _kinds.get(kind)
    if _module_info and name in _module_info:
        module_path, class_name = _module_info[name]
        module = __import__(module_path, globals(), locals(), True)
        return getattr(module, class_name)
    else:
        Adaptor = kind # TODO: look up of adaptor, for now just use kind
        try:
            Adaptee = get(name, _adaptors[kind])
        except Exception, e:
            raise Exception("could not get plugin for %s, %s: %s" % (name, kind, e))
        def const(*args, **keywords):
            return Adaptor(Adaptee(*args, **keywords))
        return const

def register_adaptor(adaptor, adaptee):
    _adaptors[adaptor] = adaptee

register_adaptor(serializer.Serializer, serializers.Serializer)
#register_adaptor(parser.Parser, parsers.Parser)

register('xml', serializers.Serializer,
         'rdflib.syntax.serializers.XMLSerializer', 'XMLSerializer')

register('pretty-xml', serializers.Serializer,
         'rdflib.syntax.serializers.PrettyXMLSerializer', 'PrettyXMLSerializer')

register('nt', serializers.Serializer,
         'rdflib.syntax.serializers.NTSerializer', 'NTSerializer')

register('xml', parsers.Parser,
         'rdflib.syntax.parsers.RDFXMLParser', 'RDFXMLParser')

register('n3', parsers.Parser,
         'rdflib.syntax.parsers.N3Parser', 'N3Parser')

register('nt', parsers.Parser,
         'rdflib.syntax.parsers.NTParser', 'NTParser')

register('n3', parsers.Parser,
         'rdflib.syntax.parsers.N3Parser', 'N3Parser')

register('default', Backend,
         'rdflib.backends.IOMemory', 'IOMemory')

register('IOMemory', Backend,
         'rdflib.backends.IOMemory', 'IOMemory')

register('Memory', Backend,
         'rdflib.backends.Memory', 'Memory')

register('Sleepycat', Backend,
         'rdflib.backends.Sleepycat', 'Sleepycat')

register('Sleepycat_new', Backend,
         'rdflib.backends.Sleepycat_new', 'Sleepycat_new')

register('ZODB', Backend,
         'rdflib.backends.ZODB', 'ZODB')

register('sqlobject', Backend,
         'rdflib.backends._sqlobject', 'SQLObject')

register('Redland', Backend,
         'rdflib.backends.Redland', 'Redland')
<|endoftext|>


<commit_before>import re
from collections import deque

from . import tags

_header_match = re.compile('([^\s:]+): *((?:\S+\s)*\S+)|'
                           '((?:\S+\s)*\S+)')
_embed_match = re.compile('\s*(-\s*)')
# '([^\s:]+(?:\s(?:\S+\s)*\S+)?)')

class Header(object):
    def __init__(self, key, left, right):
        self.key = key
        self.values = []
        self.left = left
        self.right = right

    def fit(self, left, right):
        if (self.left <= left < self.right or
                left <= self.left < right):
            self.left = min(left, self.left)
            self.right = max(left, self.right)
            return True
        return False

    def add(self, value):
        self.values.append(value)

    def export(self):
        return (self.key, self.values)

    def __repr__(self):
        return self.key + repr(self.values)

class Headers(object):
    def __init__(self):
        self.list = []

    def read(self, line):
        hit = False
        for m in _header_match.finditer(line):
            hit = True
            if m.group(1) is not None:
                # new header
                h = Header(m.group(1), m.start(1), m.end(2))
                h.add(m.group(2))
                self.list.append(h)
            else:
                # continuation
                for h in reversed(self.list):
                    if(h.fit(m.start(3), m.end(3))):
                        h.add(m.group(3))
                        break
                else:
                    # no match
                    return False
        if not hit:
            # empty line
            return False
        return True

class Embeded(object):
    def __init__(self):
        self.state = 0

    def check(self, line):
        s = 0
        e = len(line)
        c = 0
        while s < e:
            m = _embed_match.match(line, s)
            if not m:
                return False
            c += 1
            s = m.end()

        return c

    def start(self, line):
        em = self.check(line)
        if em > 3:
            self.state = 1
            self.body = []
            self.marker = em
            self.lead = line
            self.trail = None
            self.headers = Headers()
            return True

        return False

    def end(self):
        self.state = 0
        return tags.embed(self.lead, self.body, self.trail,
                          self.headers.list)

    def read(self, line):
        if self.state == 1:
            em = self.check(line)
            if em == self.marker:
                self.state = 2
                self.trail = line
            else:
                self.body.append(line)
        elif self.state == 2:
            if not self.headers.read(line):
                return self.end()

        return None

class Document(object):
    (HEAD,
     TITLE,
     LIMBO,
     PARA) = range(4)

    def __init__(self, fp):
        self.fp = fp
        self.indent = []
        self.coindent = 0
        self.state = Document.HEAD
        self.separated = False
        self.headers = Headers()
        self.embeded = Embeded()

        self._queue = deque()
        self._end = False

    def q(self, item):
        self._queue.append(item)

    def pop(self):
        while not self._queue:
            if self._end:
                return None
            line = self.fp.readline()
            if not line:
                self._end = True
                self.end()

            self.line(line)

        return self._queue.popleft()

    def __iter__(self):
        return self

    def next(self):
        n = self.pop()
        if n is None:
            raise StopIteration()
        return n

    def _clean_lists(self):
        for lvl, (xi, xt) in enumerate(self.indent):
            self.q(xt(len(self.indent) - lvl, None))
        del self.indent[:]

    def _clean_para(self):
        self._clean_lists()
        if self.state >= Document.PARA:
            self.q(tags.para(False))
            self.state = Document.LIMBO

    def end(self):
        if self.embeded.state:
            self.q(self.embeded.end())

        self._clean_para()
        self.q(tags.end())

    def line(self, line):
        if self.embeded.state:
            tag = self.embeded.read(line)
            if tag:
                self.q(tag)
            return

        line = line.rstrip().expandtabs()
        # import sys
        # sys.stderr.write('\n>>> %s\n' % line)

        if not line:
            self.separated = True
            return

        separated = self.separated
        self.separated = False

        if self.state == Document.HEAD:
            if not separated and self.headers.read(line):
                return
            self.q(tags.headers(self.headers.list))
            self.state = Document.TITLE

        if self.embeded.start(line):
            if separated:
                self._clean_para()
            return

        i = len(line)
        line = line.lstrip()
        i -= len(line)

        o = 0

        if self.state <= Document.TITLE:
            if i > 5:
                self.q(tags.title(line))
                self.state = Document.TITLE
                return

        if i > 0:
            tag = None
            lbl, o = self.ordered(line)
            if lbl:
                tag = tags.ordered
            else:
                lbl, o = self.unordered(line)
                if lbl:
                    tag = tags.unordered

            if tag:
                xi, xt = -1, None
                while self.indent:
                    xi, xt = self.indent[-1]
                    if i < xi:
                        self.q(xt(len(self.indent), None))
                        self.indent.pop()
                    else:
                        break
                else:
                    xi, xt = -1, None

                if i == xi and not xt == tag:
                    self.q(xt(len(self.indent), None))
                    self.indent.pop()
                    self.indent.append((i, tag))
                    self.q(tag(len(self.indent), lbl))
                elif i > xi:
                    if self.state == Document.LIMBO:
                        self.q(tags.para(True))
                        self.state = Document.PARA
                    self.indent.append((i, tag))
                    self.q(tag(len(self.indent), lbl))

                self.q(tags.item(line[o:]))
                self.coindent = o
                return

            if i >= self.coindent:
                self.q(tags.text(line[o:]))
                return

        # We're looking at body text or possibly a section heading,
        # clear out running lists.
        self._clean_lists()

        if self.state < Document.LIMBO:
            self.state = Document.LIMBO

        if i == 0 and line:
            level = 0
            numbered = True
            while True:
                num, o = self.num(line, o)
                if num is None:
                    break
                if not (level or int(num)):
                    numbered = False

                level += 1

                if line[o] == '.':
                    o += 1
                elif line[o].isspace():
                    break

            if level:
                o = self._skip(line, o)
                if self.state == Document.PARA:
                    self.q(tags.para(False))
                    self.state = Document.LIMBO
                self.q(tags.section(level, numbered, line[o:]))
                return

        if separated and self.state == Document.PARA:
            self.q(tags.para(False))
            self.state = Document.LIMBO

        if self.state != Document.PARA:
            self.state = Document.PARA
            self.q(tags.para(True))

        self.q(tags.text(line))

    def num(self, line, o):
        b = o
        while '0' <= line[o] <= '9':
            o += 1

        return (line[b:o] if o > b else None, o)

    def _skip(self, line, o):
        for x in line[o:]:
            if not x.isspace():
                break
            o += 1
        return o

    def _check_set(self, line, o, args):
        c = 0
        for x in line[o+1:]:
            if x in args:
                c += 1
        return c

    def ordered(self, line, o=0):
        for roman in (('i', 'v', 'x'), ('I', 'V', 'X')):
            c = self._check_set(line, o, roman)
            if c and line[o+c] == '.':
                return line[o:o+c], self._skip(line, o+c+1)

        if line[o].isalpha() and line[o+1] == '.':
            return line[o], self._skip(line, o+2)

        n, o = self.num(line, o)
        if n is not None and line[o] == '.':
            return n, self._skip(line, o+1)

        return None, o

    def unordered(self, line, o=0):
        if line[o] in ('-', '+', '*') and line[o+1].isspace():
            return line[o], self._skip(line, o+2)
        return None, o
<commit_msg>Pop lists in the correct order<commit_after>import re
from collections import deque

from . import tags

_header_match = re.compile('([^\s:]+): *((?:\S+\s)*\S+)|'
                           '((?:\S+\s)*\S+)')
_embed_match = re.compile('\s*(-\s*)')
# '([^\s:]+(?:\s(?:\S+\s)*\S+)?)')

class Header(object):
    def __init__(self, key, left, right):
        self.key = key
        self.values = []
        self.left = left
        self.right = right

    def fit(self, left, right):
        if (self.left <= left < self.right or
                left <= self.left < right):
            self.left = min(left, self.left)
            self.right = max(left, self.right)
            return True
        return False

    def add(self, value):
        self.values.append(value)

    def export(self):
        return (self.key, self.values)

    def __repr__(self):
        return self.key + repr(self.values)

class Headers(object):
    def __init__(self):
        self.list = []

    def read(self, line):
        hit = False
        for m in _header_match.finditer(line):
            hit = True
            if m.group(1) is not None:
                # new header
                h = Header(m.group(1), m.start(1), m.end(2))
                h.add(m.group(2))
                self.list.append(h)
            else:
                # continuation
                for h in reversed(self.list):
                    if(h.fit(m.start(3), m.end(3))):
                        h.add(m.group(3))
                        break
                else:
                    # no match
                    return False
        if not hit:
            # empty line
            return False
        return True

class Embeded(object):
    def __init__(self):
        self.state = 0

    def check(self, line):
        s = 0
        e = len(line)
        c = 0
        while s < e:
            m = _embed_match.match(line, s)
            if not m:
                return False
            c += 1
            s = m.end()

        return c

    def start(self, line):
        em = self.check(line)
        if em > 3:
            self.state = 1
            self.body = []
            self.marker = em
            self.lead = line
            self.trail = None
            self.headers = Headers()
            return True

        return False

    def end(self):
        self.state = 0
        return tags.embed(self.lead, self.body, self.trail,
                          self.headers.list)

    def read(self, line):
        if self.state == 1:
            em = self.check(line)
            if em == self.marker:
                self.state = 2
                self.trail = line
            else:
                self.body.append(line)
        elif self.state == 2:
            if not self.headers.read(line):
                return self.end()

        return None

class Document(object):
    (HEAD,
     TITLE,
     LIMBO,
     PARA) = range(4)

    def __init__(self, fp):
        self.fp = fp
        self.indent = []
        self.coindent = 0
        self.state = Document.HEAD
        self.separated = False
        self.headers = Headers()
        self.embeded = Embeded()

        self._queue = deque()
        self._end = False

    def q(self, item):
        self._queue.append(item)

    def pop(self):
        while not self._queue:
            if self._end:
                return None
            line = self.fp.readline()
            if not line:
                self._end = True
                self.end()

            self.line(line)

        return self._queue.popleft()

    def __iter__(self):
        return self

    def next(self):
        n = self.pop()
        if n is None:
            raise StopIteration()
        return n

    def _clean_lists(self):
        for lvl, (xi, xt) in reversed(list(enumerate(self.indent))):
            self.q(xt(len(self.indent) - lvl, None))
        del self.indent[:]

    def _clean_para(self):
        self._clean_lists()
        if self.state >= Document.PARA:
            self.q(tags.para(False))
            self.state = Document.LIMBO

    def end(self):
        if self.embeded.state:
            self.q(self.embeded.end())

        self._clean_para()
        self.q(tags.end())

    def line(self, line):
        if self.embeded.state:
            tag = self.embeded.read(line)
            if tag:
                self.q(tag)
            return

        line = line.rstrip().expandtabs()
        # import sys
        # sys.stderr.write('\n>>> %s\n' % line)

        if not line:
            self.separated = True
            return

        separated = self.separated
        self.separated = False

        if self.state == Document.HEAD:
            if not separated and self.headers.read(line):
                return
            self.q(tags.headers(self.headers.list))
            self.state = Document.TITLE

        if self.embeded.start(line):
            if separated:
                self._clean_para()
            return

        i = len(line)
        line = line.lstrip()
        i -= len(line)

        o = 0

        if self.state <= Document.TITLE:
            if i > 5:
                self.q(tags.title(line))
                self.state = Document.TITLE
                return

        if i > 0:
            tag = None
            lbl, o = self.ordered(line)
            if lbl:
                tag = tags.ordered
            else:
                lbl, o = self.unordered(line)
                if lbl:
                    tag = tags.unordered

            if tag:
                xi, xt = -1, None
                while self.indent:
                    xi, xt = self.indent[-1]
                    if i < xi:
                        self.q(xt(len(self.indent), None))
                        self.indent.pop()
                    else:
                        break
                else:
                    xi, xt = -1, None

                if i == xi and not xt == tag:
                    self.q(xt(len(self.indent), None))
                    self.indent.pop()
                    self.indent.append((i, tag))
                    self.q(tag(len(self.indent), lbl))
                elif i > xi:
                    if self.state == Document.LIMBO:
                        self.q(tags.para(True))
                        self.state = Document.PARA
                    self.indent.append((i, tag))
                    self.q(tag(len(self.indent), lbl))

                self.q(tags.item(line[o:]))
                self.coindent = o
                return

            if i >= self.coindent:
                self.q(tags.text(line[o:]))
                return

        # We're looking at body text or possibly a section heading,
        # clear out running lists.
        self._clean_lists()

        if self.state < Document.LIMBO:
            self.state = Document.LIMBO

        if i == 0 and line:
            level = 0
            numbered = True
            while True:
                num, o = self.num(line, o)
                if num is None:
                    break
                if not (level or int(num)):
                    numbered = False

                level += 1

                if line[o] == '.':
                    o += 1
                elif line[o].isspace():
                    break

            if level:
                o = self._skip(line, o)
                if self.state == Document.PARA:
                    self.q(tags.para(False))
                    self.state = Document.LIMBO
                self.q(tags.section(level, numbered, line[o:]))
                return

        if separated and self.state == Document.PARA:
            self.q(tags.para(False))
            self.state = Document.LIMBO

        if self.state != Document.PARA:
            self.state = Document.PARA
            self.q(tags.para(True))

        self.q(tags.text(line))

    def num(self, line, o):
        b = o
        while '0' <= line[o] <= '9':
            o += 1

        return (line[b:o] if o > b else None, o)

    def _skip(self, line, o):
        for x in line[o:]:
            if not x.isspace():
                break
            o += 1
        return o

    def _check_set(self, line, o, args):
        c = 0
        for x in line[o+1:]:
            if x in args:
                c += 1
        return c

    def ordered(self, line, o=0):
        for roman in (('i', 'v', 'x'), ('I', 'V', 'X')):
            c = self._check_set(line, o, roman)
            if c and line[o+c] == '.':
                return line[o:o+c], self._skip(line, o+c+1)

        if line[o].isalpha() and line[o+1] == '.':
            return line[o], self._skip(line, o+2)

        n, o = self.num(line, o)
        if n is not None and line[o] == '.':
            return n, self._skip(line, o+1)

        return None, o

    def unordered(self, line, o=0):
        if line[o] in ('-', '+', '*') and line[o+1].isspace():
            return line[o], self._skip(line, o+2)
        return None, o
<|endoftext|>