Source code for campos.fields

import os
import operator
from datetime import date, time, datetime

import qtpy.QtWidgets as Qt
from qtpy.QtCore import QDate, QTime, QDateTime

from .core import BaseField
from .utils import first_of_type, callable
from .validators import (NumberRange, StringLength, DateRange, TimeRange,
                         DatetimeRange)

__author__ = 'Juan Manuel Bermúdez Cabrera'


[docs]class IntField(BaseField): """Field to introduce :class:`int` values :param min: minimum admitted value, defaults to 0 :type min: :class:`int` :param max: maximum admitted value, defaults to 100 :type min: :class:`int` :param step: amount to increase or decrease current value by, defaults to 1 :type min: :class:`int` """ def __init__(self, *args, min=0, max=100, step=1, **kwargs): self._spin = Qt.QSpinBox() kwargs.setdefault('default', 0) super(IntField, self).__init__(*args, **kwargs) self._range = first_of_type(self.validators, NumberRange) if self._range is None: self._range = NumberRange(min=min, max=max) self.validators.append(self._range) self.min = self._range.min self.max = self._range.max self.step = step @property def main_component(self): return self._spin @property def change_signal(self): return self.main_component.valueChanged @property def value(self): return self.main_component.value() @value.setter def value(self, value): self.main_component.setValue(value) def has_data(self): return True @property def step(self): """Amount to increase or decrease current value by. :type: :class:`int` """ return self.main_component.singleStep() @step.setter def step(self, value): self.main_component.setSingleStep(value) @property def min(self): """Minimum admitted value. :type: :class:`int` """ return self._range.min @min.setter def min(self, value): self._range.min = value self.main_component.setMinimum(value) @property def max(self): """Maximum admitted value. :type: :class:`int` """ return self._range.max @max.setter def max(self, value): self._range.max = value self.main_component.setMaximum(value)
[docs]class FloatField(IntField): """Field to introduce :class:`float` values :param precision: decimal places, defaults to 2 :type min: :class:`int` """ def __init__(self, *args, precision=2, **kwargs): self._double_spin = Qt.QDoubleSpinBox() kwargs.setdefault('default', 0) super(FloatField, self).__init__(*args, **kwargs) self.precision = precision self.value = self.default @property def main_component(self): return self._double_spin @property def precision(self): """Decimal places. :type: :class:`int` """ return self.main_component.decimals() @precision.setter def precision(self, value): self.main_component.setDecimals(value)
[docs]class StringField(BaseField): """Field to introduce :class:`str` :param min_length: minimum admitted length, defaults to 0 :type min_length: :class:`int` :param max_length: maximum admitted length, defaults to 100 :type max_length: :class:`int` """ def __init__(self, *args, min_length=0, max_length=100, **kwargs): self._editor = Qt.QLineEdit() kwargs.setdefault('default', '') super(StringField, self).__init__(*args, **kwargs) self._length = first_of_type(self.validators, StringLength) if self._length is None: self._length = StringLength(min=min_length, max=max_length) self.validators.append(self._length) self.min_length = self._length.min self.max_length = self._length.max @property def main_component(self): return self._editor @property def change_signal(self): return self.main_component.textChanged @property def value(self): return self.main_component.text() @value.setter def value(self, new): self.main_component.setText(new) def has_data(self): return len(self.value) > 0 @property def min_length(self): """Minimum admitted length. :type: :class:`int` """ return self._length.min @min_length.setter def min_length(self, value): if value < 0: msg = 'Expecting non negative number, got {}' raise ValueError(msg.format(value)) self._length.min = value @property def max_length(self): """Maximum admitted length. :type: :class:`int` """ return self._length.max @max_length.setter def max_length(self, value): if value < 0: msg = 'Expecting non negative number, got {}' raise ValueError(msg.format(value)) self._length.max = value self.main_component.setMaxLength(value)
[docs]class TextField(StringField): """Field to introduce large strings""" def __init__(self, *args, **kwargs): self._text_editor = Qt.QTextEdit() kwargs.setdefault('default', '') kwargs.setdefault('max_length', 1000) super(TextField, self).__init__(*args, **kwargs) @property def main_component(self): return self._text_editor @property def value(self): return self.main_component.toPlainText() @value.setter def value(self, value): self.main_component.setPlainText(value) @property def max_length(self): """Maximum admitted length. :type: :class:`int` """ return self._length.max @max_length.setter def max_length(self, value): if value < 0: msg = 'Expecting non negative number, got {}' raise ValueError(msg.format(value)) self._length.max = value
[docs]class BoolField(BaseField): """Field to ask for yes or no input""" def __init__(self, *args, **kwargs): self._box = Qt.QCheckBox('') kwargs.setdefault('default', False) super(BoolField, self).__init__(*args, **kwargs) @property def main_component(self): return self._box @property def change_signal(self): return self._box.stateChanged @property def value(self): return self.main_component.isChecked() @value.setter def value(self, value): self.main_component.setChecked(value) def has_data(self): return True
[docs]class DateField(BaseField): """Field to introduce :class:`datetime.date` values. :param format: Qt's format string used to show the current value and to convert values assigned to `min` , `max` and `value` . Defaults to 'dd/MM/yyyy' :type format: :class:`str` :param min: minimum admitted date, defaults to :func:`datetime.date.today` :type min: :class:`datetime.date` or :class:`str` :param max: maximum admitted date, defaults to :attr:`datetime.date.max` :type max: :class:`datetime.date` or :class:`str` .. note:: if the values passed to `min` , `max` or `value` are strings then a date object is parsed using the current `format` """ def __init__(self, *args, format='dd/MM/yyyy', min=date.today(), max=date.max, **kwargs): self._editor = Qt.QDateEdit() self._editor.setCalendarPopup(True) kwargs.setdefault('default', date.today()) super(DateField, self).__init__(*args, **kwargs) self.format = format self._range = first_of_type(self.validators, DateRange) if self._range is None: self._range = DateRange() self.min = min self.max = max else: self.min = self._range.min self.max = self._range.max self.validators.append(self._range) @property def main_component(self): return self._editor @property def change_signal(self): return self.main_component.dateChanged @property def value(self): return self._to_date(self.main_component.date()) @value.setter def value(self, value): self.main_component.setDate(self._to_date(value)) def has_data(self): return True @property def format(self): """Qt's date format string used to show the date in the widget and to parse string values assigned to `min`, `max` and `value`. :type: :class:`str` """ return self.main_component.displayFormat() @format.setter def format(self, value): self.main_component.setDisplayFormat(value) @property def min(self): """Minimum admitted date. :type: :class:`datetime.date` or date string .. note:: if the value passed to `min` is a string then a date is parsed using current `format` """ return self._range.min @min.setter def min(self, value): py_date = self._to_date(value) self.main_component.setMinimumDate(py_date) self._range.min = py_date @property def max(self): """Maximum admitted date. :type: :class:`datetime.date` or date string .. note:: if the value passed to `max` is a string then a date is parsed using current `format` """ return self._range.max @max.setter def max(self, value): py_date = self._to_date(value) self.main_component.setMaximumDate(py_date) self._range.max = py_date def _to_date(self, value): qdate = value if isinstance(qdate, str): qdate = QDate.fromString(value, self.format) elif not isinstance(qdate, date): qdate = date(qdate.year(), qdate.month(), qdate.day()) return qdate
[docs]class TimeField(BaseField): """Field to introduce :class:`datetime.time` values. :param format: Qt's format string used to show the current value and to convert values assigned to `min` , `max` and `value` . Defaults to 'HH:mm:ss' :type format: :class:`str` :param min: minimum admitted time, defaults to :attr:`datetime.time.min` :type min: :class:`datetime.time` or :class:`str` :param max: maximum admitted time, defaults to :attr:`datetime.time.max` :type max: :class:`datetime.time` or :class:`str` .. note:: if the values passed to `min` , `max` or `value` are strings then a time object is parsed using the current `format` """ def __init__(self, *args, format='HH:mm:ss', min=time.min, max=time.max, **kwargs): self._editor = Qt.QTimeEdit() kwargs.setdefault('default', datetime.now().time()) super(TimeField, self).__init__(*args, **kwargs) self.format = format self._range = first_of_type(self.validators, TimeRange) if self._range is None: self._range = TimeRange() self.min = min self.max = max else: self.min = self._range.min self.max = self._range.max self.validators.append(self._range) @property def main_component(self): return self._editor @property def change_signal(self): return self.main_component.timeChanged @property def value(self): return self._to_time(self.main_component.time()) @value.setter def value(self, value): self.main_component.setTime(self._to_time(value)) def has_data(self): return True @property def format(self): """Qt's time format string used to show the time in the widget and to parse string values assigned to `min`, `max` and `value`. :type: :class:`str` """ return self.main_component.displayFormat() @format.setter def format(self, value): self.main_component.setDisplayFormat(value) @property def min(self): """Minimum admitted time. :type: :class:`datetime.time` or time string .. note:: if the value passed to `min` is a string then a time is parsed using the current `format` """ return self._range.min @min.setter def min(self, value): # first convert input to a valid time object since value can be a string py_time = self._to_time(value ) self.main_component.setMinimumTime(py_time) self._range.min = py_time @property def max(self): """Maximum admitted time. :type: :class:`datetime.time` or time string .. note:: if the value passed to `max` is a string then a time is parsed using the current `format` """ return self._range.max @max.setter def max(self, value): # first convert input to a valid time object since value can be a string py_time = self._to_time(value) self.main_component.setMaximumTime(py_time) self._range.max = py_time def _to_time(self, value): qtime = value if isinstance(qtime, str): qtime = QTime.fromString(value, self.format) elif not isinstance(qtime, time): qtime = time(qtime.hour(), qtime.minute(), qtime.second(), qtime.msec()) return qtime
[docs]class DatetimeField(BaseField): """Field to introduce :class:`datetime.datetime` values. :param format: Qt's format string used to show current value and to convert values assigned to `min` , `max` and `value` . Defaults to 'dd/MM/yyyy HH:mm:ss' :type format: :class:`str` :param min: minimum admitted datetime, defaults to :attr:`datetime.datetime.min` :type min: :class:`datetime.datetime` or :class:`str` :param max: maximum admitted datetime, defaults to :attr:`datetime.datetime.max` :type max: :class:`datetime.datetime` or :class:`str` .. note:: if the values passed to `min` , `max` or `value` are strings then a datetime object is parsed using current format """ def __init__(self, *args, format='dd/MM/yyyy HH:mm:ss', min=datetime.min, max=datetime.max, **kwargs): self._editor = Qt.QDateTimeEdit() self._editor.setCalendarPopup(True) kwargs.setdefault('default', datetime.now()) super(DatetimeField, self).__init__(*args, **kwargs) self.format = format self._range = first_of_type(self.validators, DatetimeRange) if self._range is None: self._range = DatetimeRange() self.min = min self.max = max else: self.min = self._range.min self.max = self._range.max self.validators.append(self._range) @property def main_component(self): return self._editor @property def change_signal(self): return self.main_component.dateTimeChanged @property def min(self): """Minimum admitted datetime. :type: :class:`datetime.datetime` or datetime string .. note:: if the value passed to `min` is a string then a datetime is parsed using current format """ return self._range.min @min.setter def min(self, value): dt = self._to_datetime(value) self.main_component.setMinimumDateTime(dt) self._range.min = dt @property def max(self): """Maximum admitted datetime. :type: :class:`datetime.datetime` or datetime string .. note:: if the value passed to `max` is a string then a datetime is parsed using current format """ return self._range.max @max.setter def max(self, value): dt = self._to_datetime(value) self.main_component.setMaximumDateTime(dt) self._range.max = dt @property def format(self): """Qt's format string used to show current value and to convert values assigned to `min` , `max` and `value` . :type: :class:`str` """ return self.main_component.displayFormat() @format.setter def format(self, value): self.main_component.setDisplayFormat(value) @property def value(self): return self._to_datetime(self.main_component.dateTime()) @value.setter def value(self, value): self.main_component.setDateTime(self._to_datetime(value)) def has_data(self): return True def _to_datetime(self, value): qdatetime = value if isinstance(qdatetime, str): qdatetime = QDateTime.fromString(value, self.format) elif not isinstance(qdatetime, datetime): qdate, qtime = qdatetime.date(), qdatetime.time() qdatetime = datetime(qdate.year(), qdate.month(), qdate.day(), qtime.hour(), qtime.minute(), qtime.second(), qtime.msec()) return qdatetime
[docs]class SelectField(BaseField): """Field to select an option among several ones. The value of this field is a :class:`tuple` with the text of the selected option at index 0 and its value at index 1. `choices` argument can be an iterable or a callable that yields an iterable and its members can adopt several shapes: * If is an string then that's the option's text and value. * If is a subscriptable object then the text is expected at index 0 and value at index 1 defaulting to index 0 if is not reachable. * If is other kind of object then the text is it :func:`str` result and value is the object itself. .. note:: previous rules only apply for option's text or value if `get_text` or `get_value` aren't defined: :param choices: options to show :type choices: iterable or callable :param blank: whether to show or not an option meaning no selection. :type blank: :class:`bool` :param blank_text: text to show in the meaningless option(value is equal to text too) :type blank_text: :class:`str` :param get_text: used to obtain option's text, can be a callable to invoke using each `choices` member as first argument or a string indicating the name of the attribute to read from them. :type get_text: callable or :class:`str` :param get_value: used to obtain option's value, can be a callable to invoke using each `choices` member as first argument or a string indicating the name of the attribute to read from them. :type get_text: callable or :class:`str` """ def __init__(self, *args, choices=(), blank=False, blank_text='', get_text=None, get_value=None, **kwargs): self._combo = Qt.QComboBox() self._combo.setEditable(False) self._text_getter = self._create_text_getter(get_text) self._value_getter = self._create_value_getter(get_value) self.choices = [] self._blank_present = blank self._blank_text = blank_text if blank: self.add_choice(blank_text, blank_text) for ch in choices() if callable(choices) else choices: text = self._text_getter(ch) value = self._value_getter(ch) self.add_choice(text, value) if kwargs.get('default') is None: default = self.choices[0] if self.choices else ('', '') kwargs['default'] = default[0] super(SelectField, self).__init__(*args, **kwargs) @property def main_component(self): return self._combo @property def change_signal(self): return self.main_component.currentIndexChanged @property def value(self): """The selected option. .. note:: To change the current selection you can pass only the new option's text or a tuple like ``(option's text, option's value)``. :return: a :class:`tuple` like ``(option's text, option's value)`` :rtype: :class:`tuple` """ component = self.main_component index = component.currentIndex() return component.currentText(), component.itemData(index) @value.setter def value(self, new): if isinstance(new, str): text = new else: text, _ = new index = self.main_component.findText(text) if index < 0 and self.choices: raise ValueError('No choice with text {}'.format(text)) self.main_component.setCurrentIndex(index) def has_data(self): return self.main_component.currentIndex() >= 0
[docs] def add_choice(self, text, value): """Adds a new choice to the options list. :param text: text of the new option :type text: :class:`str` :param value: value of the new option :type value: any """ self.choices.append((text, value)) self.main_component.addItem(text, value)
[docs] def clear(self): """Removes all options except the blank one if present""" self.main_component.clear() self.choices.clear() if self._blank_present: self.add_choice(self._blank_text, self._blank_text)
@staticmethod def _create_text_getter(arg): if callable(arg): getter = arg elif isinstance(arg, str): getter = operator.attrgetter(arg) else: def getter(e): # choice can be an string if isinstance(e, str): return e # at this point e must be a subscriptable object with the text # value at index 0 try: return e[0] except TypeError: # non subscriptable object, the output is it str() return str(e) return getter @staticmethod def _create_value_getter(arg): if callable(arg): getter = arg elif isinstance(arg, str): getter = operator.attrgetter(arg) else: def getter(e): # choice can be an string if isinstance(e, str): return e try: return e[1] # try to get the value at index 1 except IndexError: return e[0] # value not present so use text instead except TypeError: # non subscriptable object, the output is the same element return e return getter
[docs]class FileField(BaseField): """Field to input file(s). File paths can be entered manually and are separated by ``PATH_SEP``. The value of this field is always a list of paths, independently of the value of `multi_select` :param multi_select: whether to allow or not selection of several files :type multi_select: :class:`bool` :param chooser_title: text to show in the file chooser :type chooser_title: :class:`str` :param button_text: text to show in the file chooser invoker button :type button_text: :class:`str` .. seealso:: :class:`DirField` """ PATHS_SEP = ';' def __init__(self, *args, multi_select=False, chooser_title='Choose a file', button_text='Browse', **kwargs): # text field to show selected file(s) path(s) self._string = StringField() self._string.text = '' self._file_chooser = Qt.QFileDialog() self._browse = Qt.QPushButton('') self._browse.clicked.connect(self._file_chooser.exec) self._layout = Qt.QHBoxLayout() self._layout.addWidget(self._string, stretch=1) self._layout.addWidget(self._browse) kwargs.setdefault('default', []) super(FileField, self).__init__(*args, **kwargs) def _close_cb(): joined = self.PATHS_SEP.join(self._file_chooser.selectedFiles()) if len(joined) > self._string.max_length: self._string.max_length = len(joined) self._string.value = joined class _PathValidator: def __call__(self, field): if len(field.value) > 0: if not all(os.path.isfile(p) for p in field.value): raise ValueError('Invalid file(s) found') self._file_chooser.finished.connect(_close_cb) self.validators.append(_PathValidator()) self.chooser_title = chooser_title self.button_text = button_text self._multi_select = None self.multi_select = multi_select @property def main_component(self): return self._layout @property def change_signal(self): return self._string.change_signal @property def value(self): paths = self._string.value return paths.split(self.PATHS_SEP) if paths else [] @value.setter def value(self, value): self._string.value = self.PATHS_SEP.join(value) def has_data(self): return self._string.has_data() @property def chooser_title(self): """Text to show in the file chooser. :type: :class:`str` """ return self._file_chooser.windowTitle() @chooser_title.setter def chooser_title(self, value): self._file_chooser.setWindowTitle(value) @property def button_text(self): """Text to show in the file chooser invoker button. :type: :class:`str` """ return self._browse.text() @button_text.setter def button_text(self, value): self._browse.setText(value) @property def multi_select(self): """Whether to allow or not selection of several files. :type: :class:`bool` """ return self._multi_select @multi_select.setter def multi_select(self, value): self._multi_select = value if value: self._file_chooser.setFileMode(Qt.QFileDialog.ExistingFiles) else: self._file_chooser.setFileMode(Qt.QFileDialog.ExistingFile)
[docs] def add_filter(self, name, patterns): """Adds a named filter to this field. Filters do not apply to manually entered paths. For instance, if you want to show the following filters:: Image files (*.png *.jpg) Text files (*.txt) Any files (*) You can add them like this:: fi = FileField() fi.add_filter('Image files', ['*.png', '*.jpg']) fi.add_filter('Text files', ['*.txt']) fi.add_filter('Any files', ['*']) :param name: a string identifying the filter :type name: :class:`str` :param patterns: a collection of Qt's filename-wildcard patterns :type patterns: iterable of strings """ new = '{} ({})'.format(name, ' '.join(patterns)) filters = self._file_chooser.nameFilters() filters.append(new) self._file_chooser.setNameFilters(filters)
[docs]class DirField(BaseField): """Field to input a directory path. Dir path can be entered manually. :param chooser_title: text to show in the directory chooser :type chooser_title: :class:`str` :param button_text: text to show in the directory chooser invoker button :type button_text: :class:`str` .. seealso:: :class:`FileInput` """ def __init__(self, *args, chooser_title='Choose a directory', button_text='Browse', **kwargs): # text widget to show selected dir path self._string = StringField() self._string.text = '' self._dir_chooser = Qt.QFileDialog() self._dir_chooser.setFileMode(Qt.QFileDialog.Directory) self._dir_chooser.setOption(Qt.QFileDialog.ShowDirsOnly, True) self._browse = Qt.QPushButton() self._browse.clicked.connect(self._dir_chooser.exec) self._layout = Qt.QHBoxLayout() self._layout.addWidget(self._string, stretch=1) self._layout.addWidget(self._browse) kwargs.setdefault('default', '') super(DirField, self).__init__(*args, **kwargs) def _close_cb(): path = self._dir_chooser.directory().absolutePath() if len(path) > self._string.max_length: self._string.max_length = len(path) self._string.value = path class _PathValidator: def __call__(self, field): if len(field.value) > 0 and not os.path.isdir(field.value): raise ValueError('Invalid path') self._dir_chooser.finished.connect(_close_cb) self.validators.append(_PathValidator()) self.chooser_title = chooser_title self.button_text = button_text @property def main_component(self): return self._layout @property def change_signal(self): return self._string.change_signal @property def value(self): return self._string.value @value.setter def value(self, value): self._string.value = value def has_data(self): return self._string.has_data() @property def chooser_title(self): """Text to show in the directory chooser. :type: :class:`str` """ return self._dir_chooser.windowTitle() @chooser_title.setter def chooser_title(self, value): self._dir_chooser.setWindowTitle(value) @property def button_text(self): """Text to show in the directory chooser invoker button. :type: :class:`str` """ return self._browse.text() @button_text.setter def button_text(self, value): self._browse.setText(value)