<commit_before>"""
HTML form fields.
"""

import abc
import six

from robobrowser.compat import string_types
from robobrowser import helpers
from robobrowser import exceptions


class ValueMeta(type):
    """Metaclass that creates a value property on class creation. Classes
    with this metaclass should define _get_value and optionally _set_value
    methods.

    """
    def __init__(cls, name, bases, dct):
        cls.value = property(
            getattr(cls, '_get_value'),
            getattr(cls, '_set_value', None),
        )
        super(ValueMeta, cls).__init__(name, bases, dct)


class FieldMeta(ValueMeta, abc.ABCMeta):
    """Multiply inherit from ValueMeta and ABCMeta; classes with this metaclass
    are automatically assigned a value property and can use methods from
    ABCMeta (e.g. abstractmethod).

    """
    pass


@six.add_metaclass(FieldMeta)
class BaseField(object):
    """Abstract base class for form fields.

    :param parsed: String or BeautifulSoup tag

    """
    def __init__(self, parsed):
        self._parsed = helpers.ensure_soup(parsed, parser='html.parser')
        self._value = None
        self.name = self._get_name(self._parsed)

    @property
    def disabled(self):
        return 'disabled' in self._parsed.attrs

    def _get_name(self, parsed):
        name = parsed.get('name')
        if name is not None:
            return name
        raise exceptions.InvalidNameError

    # Different form fields may serialize their values under different keys.
    # See `FormData` for details.
    payload_key = None

    def serialize(self):
        return {self.name: self.value}

    # Property methods
    def _get_value(self):
        return self._value if self._value else ''

    def _set_value(self, value):
        self._value = value


class Input(BaseField):

    def __init__(self, parsed):
        super(Input, self).__init__(parsed)
        self.value = self._parsed.get('value')


class Submit(Input):
    pass


class FileInput(BaseField):

    def _set_value(self, value):
        if hasattr(value, 'read'):
            self._value = value
        elif isinstance(value, string_types):
            self._value = open(value)
        else:
            raise ValueError('Value must be a file object or file path')

    # Serialize value to 'files' key for compatibility with file attachments
    # in requests.
    payload_key = 'files'


class MultiOptionField(BaseField):

    @abc.abstractproperty
    def default_value(self):
        """When the "value" attribute is not defined for a multi-option form
        field, some default must be used instead.

        """
        return None

    def __init__(self, parsed):
        super(MultiOptionField, self).__init__(parsed)
        self.options, self.labels, initial = self._get_options(parsed)
        self._set_initial(initial)

    @abc.abstractmethod
    def _get_options(self, parsed):
        return [], [], []

    def _set_initial(self, initial):
        self._value = None
        try:
            self.value = initial[0]
        except IndexError:
            pass

    def _value_to_index(self, value):
        if value in self.options:
            return self.options.index(value)
        if value in self.labels:
            index = self.labels.index(value)
            if index not in self.labels[index:]:
                return index
        raise ValueError('Option "{o}" does not exist in multi-option form '
                         'field "{f}".'.format(o=value, f=self.name))

    # Property methods
    def _get_value(self):
        if self._value is None:
            return ''
        return self.options[self._value]

    def _set_value(self, value):
        self._value = self._value_to_index(value)


class MultiValueField(MultiOptionField):

    def _set_initial(self, initial):
        self.value = initial

    # Property methods
    def _get_value(self):
        return [
            self.options[idx]
            for idx in self._value
        ]

    def _set_value(self, value):
        if not isinstance(value, list):
            value = [value]
        self._value = [
            self._value_to_index(item)
            for item in value
        ]

    # List-like methods
    def append(self, value):
        index = self._value_to_index(value)
        if index in self._value:
            raise ValueError
        self._value.append(index)
        self._value.sort()

    def remove(self, value):
        index = self._value_to_index(value)
        self._value.remove(index)


class FlatOptionField(MultiOptionField):

    @property
    def disabled(self):
        return all('disabled' in each.attrs for each in self._parsed)

    def _get_name(self, parsed):
        name = parsed[0].get('name')
        if name is not None:
            return name
        raise exceptions.InvalidNameError

    def _get_options(self, parsed):
        options, labels, initial = [], [], []
        for option in parsed:
            value = option.get('value', self.default_value)
            checked = option.get('checked')
            options.append(value)
            labels.append(
                option.next.string
                if isinstance(option.next, string_types)
                else None
            )
            if checked is not None:
                initial.append(value)
        return options, labels, initial


class NestedOptionField(MultiOptionField):

    @property<commit_msg>Add better error messages.<commit_after>"""
HTML form fields.
"""

import abc
import six

from robobrowser.compat import string_types
from robobrowser import helpers
from robobrowser import exceptions


class ValueMeta(type):
    """Metaclass that creates a value property on class creation. Classes
    with this metaclass should define _get_value and optionally _set_value
    methods.

    """
    def __init__(cls, name, bases, dct):
        cls.value = property(
            getattr(cls, '_get_value'),
            getattr(cls, '_set_value', None),
        )
        super(ValueMeta, cls).__init__(name, bases, dct)


class FieldMeta(ValueMeta, abc.ABCMeta):
    """Multiply inherit from ValueMeta and ABCMeta; classes with this metaclass
    are automatically assigned a value property and can use methods from
    ABCMeta (e.g. abstractmethod).

    """
    pass


@six.add_metaclass(FieldMeta)
class BaseField(object):
    """Abstract base class for form fields.

    :param parsed: String or BeautifulSoup tag

    """
    def __init__(self, parsed):
        self._parsed = helpers.ensure_soup(parsed, parser='html.parser')
        self._value = None
        self.name = self._get_name(self._parsed)

    @property
    def disabled(self):
        return 'disabled' in self._parsed.attrs

    def _get_name(self, parsed):
        name = parsed.get('name')
        if name is not None:
            return name
        raise exceptions.InvalidNameError

    # Different form fields may serialize their values under different keys.
    # See `FormData` for details.
    payload_key = None

    def serialize(self):
        return {self.name: self.value}

    # Property methods
    def _get_value(self):
        return self._value if self._value else ''

    def _set_value(self, value):
        self._value = value


class Input(BaseField):

    def __init__(self, parsed):
        super(Input, self).__init__(parsed)
        self.value = self._parsed.get('value')


class Submit(Input):
    pass


class FileInput(BaseField):

    def _set_value(self, value):
        if hasattr(value, 'read'):
            self._value = value
        elif isinstance(value, string_types):
            self._value = open(value)
        else:
            raise ValueError('Value must be a file object or file path')

    # Serialize value to 'files' key for compatibility with file attachments
    # in requests.
    payload_key = 'files'


class MultiOptionField(BaseField):

    @abc.abstractproperty
    def default_value(self):
        """When the "value" attribute is not defined for a multi-option form
        field, some default must be used instead.

        """
        return None

    def __init__(self, parsed):
        super(MultiOptionField, self).__init__(parsed)
        self.options, self.labels, initial = self._get_options(parsed)
        self._set_initial(initial)

    @abc.abstractmethod
    def _get_options(self, parsed):
        return [], [], []

    def _set_initial(self, initial):
        self._value = None
        try:
            self.value = initial[0]
        except IndexError:
            pass

    def _value_to_index(self, value):
        if value in self.options:
            return self.options.index(value)
        if value in self.labels:
            return self.labels.index(value)
        raise ValueError('Option {0} not found in field {1!r}'.format(value, self))

    # Property methods
    def _get_value(self):
        if self._value is None:
            return ''
        return self.options[self._value]

    def _set_value(self, value):
        self._value = self._value_to_index(value)


class MultiValueField(MultiOptionField):

    def _set_initial(self, initial):
        self.value = initial

    # Property methods
    def _get_value(self):
        return [
            self.options[idx]
            for idx in self._value
        ]

    def _set_value(self, value):
        if not isinstance(value, list):
            value = [value]
        self._value = [
            self._value_to_index(item)
            for item in value
        ]

    # List-like methods
    def append(self, value):
        index = self._value_to_index(value)
        if index in self._value:
            raise ValueError('Option {0} already in field {1!r}'.format(value, self))
        self._value.append(index)
        self._value.sort()

    def remove(self, value):
        index = self._value_to_index(value)
        self._value.remove(index)


class FlatOptionField(MultiOptionField):

    @property
    def disabled(self):
        return all('disabled' in each.attrs for each in self._parsed)

    def _get_name(self, parsed):
        return super(FlatOptionField, self)._get_name(parsed[0])

    def _get_options(self, parsed):
        options, labels, initial = [], [], []
        for option in parsed:
            value = option.get('value', self.default_value)
            checked = option.get('checked')
            options.append(value)
            labels.append(
                option.next.string
                if isinstance(option.next, string_types)
                else None
            )
            if checked is not None:
                initial.append(value)
        return options, labels, initial


class NestedOptionField(MultiOptionField):

    @property
{'fraction_non_alphanumeric': 0.09210159084566007, 'fraction_numerical': 0.0007442552795608894, 'mean_word_length': 2.298557839828168, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2732412', 'n_tokens_mistral': 3019, 'n_tokens_neox': 2949, 'n_words': 862}