def generate_choices(rule): choices = [] # Split rule into tokens, ignoring white space. tokens = _tools.tokenize_without_space(rule) # Extract choices from rule tokens. # TODO: Handle comma after comma without choice. # previous_toky = None toky = next(tokens) while not _tools.is_eof_token(toky): if _tools.is_comma_token(toky): # TODO: Handle comma after comma without choice. # if previous_toky: # previous_toky_text = previous_toky[1] # else: # previous_toky_text = None pass choice = _tools.token_text(toky) choices.append(choice) toky = next(tokens) if not _tools.is_eof_token(toky): # Process next choice after comma. toky = next(tokens) return choices
def __init__(self, field_name, is_allowed_to_be_empty, length, rule, data_format): super(ConstantFieldFormat, self).__init__( field_name, is_allowed_to_be_empty, length, rule, data_format, empty_value='') # Extract constant from rule tokens. tokens = _tools.tokenize_without_space(rule) toky = next(tokens) if _tools.is_eof_token(toky): # No rule means that the field must always be empty. self._constant = '' else: self._constant = _tools.token_text(toky) toky = next(tokens) if not _tools.is_eof_token(toky): raise errors.InterfaceError( 'constant rule must be a single Python token but also found: %s' % _compat.text_repr(_tools.token_text(toky))) has_empty_rule = (rule == '') if self.is_allowed_to_be_empty and not has_empty_rule: raise errors.InterfaceError( 'to describe a Constant that can be empty, use a Choice field with a single choice') if not self.is_allowed_to_be_empty and has_empty_rule: raise errors.InterfaceError( 'field must be marked as empty to describe a constant empty value') try: self.length.validate( 'rule of constant field %s' % _compat.text_repr(self.field_name), len(self._constant)) except errors.RangeValueError: raise errors.InterfaceError( 'length is %s but must be %d to match constant %s' % (self.length, len(self._constant), _compat.text_repr(self._constant)))
def __init__(self, field_name, is_allowed_to_be_empty, length, rule, data_format): super(ConstantFieldFormat, self).__init__( field_name, is_allowed_to_be_empty, length, rule, data_format, empty_value='') # Extract constant from rule tokens. tokens = _tools.tokenize_without_space(rule) toky = next(tokens) if _tools.is_eof_token(toky): # No rule means that the field must always be empty. self._constant = '' else: self._constant = _tools.token_text(toky) toky = next(tokens) if not _tools.is_eof_token(toky): raise errors.InterfaceError( _('constant rule must be a single Python token but also found: %s') % _compat.text_repr(_tools.token_text(toky))) has_empty_rule = (rule == '') if self.is_allowed_to_be_empty and not has_empty_rule: raise errors.InterfaceError( _('to describe a Constant that can be empty, use a Choice field with a single choice')) if not self.is_allowed_to_be_empty and has_empty_rule: raise errors.InterfaceError( _('field must be marked as empty to describe a constant empty value')) try: self.length.validate( _('rule of constant field %s') % _compat.text_repr(self.field_name), len(self._constant)) except errors.RangeValueError: raise errors.InterfaceError( _('length is %s but must be %d to match constant %s') % (self.length, len(self._constant), _compat.text_repr(self._constant)))
def _validated_character(key, value, location): r""" A single character intended as value for data format property ``key`` derived from ``value``, which can be: * a decimal or hex number (prefixed with ``'0x'``) referring to the ASCII/Unicode of the character * a string containing a single character such as ``'\t'``. * a symbolic name from :py:const:`cutplace.errors.NAME_TO_ASCII_CODE_MAP` such as ``tab``. :raises cutplace.errors.InterfaceError: on any broken ``value`` """ assert key assert value is not None name_for_errors = 'data format property %s' % _compat.text_repr(key) stripped_value = value.strip() if (len(stripped_value) == 1) and (stripped_value not in string.digits): result_code = ord(stripped_value) else: tokens = tokenize.generate_tokens(io.StringIO(value).readline) next_token = next(tokens) if _tools.is_eof_token(next_token): raise errors.InterfaceError( _("value for %s must be specified") % name_for_errors, location) next_type = next_token[0] next_value = next_token[1] if next_type == token.NAME: result_code = ranges.code_for_symbolic_token( name_for_errors, next_value, location) elif next_type == token.NUMBER: result_code = ranges.code_for_number_token( name_for_errors, next_value, location) elif next_type == token.STRING: result_code = ranges.code_for_string_token( name_for_errors, next_value, location) elif (len(next_value) == 1) and not _tools.is_eof_token(next_token): result_code = ord(next_value) else: raise errors.InterfaceError( _('value for %s must a number, a single character or a symbolic name but is: %s' ) % (name_for_errors, _compat.text_repr(value)), location) # Ensure there are no further tokens. next_token = next(tokens) if (not _tools.is_eof_token(next_token)) and (next_token[0] != tokenize.NEWLINE): raise errors.InterfaceError( _('value for %s must be a single character but is: %s') % (name_for_errors, _compat.text_repr(value)), location) # TODO: Handle 'none' properly. assert result_code is not None assert result_code >= 0 result = six.unichr(result_code) assert result is not None return result
def _validated_character(key, value, location): r""" A single character intended as value for data format property ``key`` derived from ``value``, which can be: * a decimal or hex number (prefixed with ``'0x'``) referring to the ASCII/Unicode of the character * a string containing a single character such as ``'\t'``. * a symbolic name from :py:const:`cutplace.errors.NAME_TO_ASCII_CODE_MAP` such as ``tab``. :raises cutplace.errors.InterfaceError: on any broken ``value`` """ assert key assert value is not None name_for_errors = 'data format property %s' % _compat.text_repr(key) stripped_value = value.strip() if (len(stripped_value) == 1) and (stripped_value not in string.digits): result_code = ord(stripped_value) else: tokens = tokenize.generate_tokens(io.StringIO(value).readline) next_token = next(tokens) if _tools.is_eof_token(next_token): raise errors.InterfaceError( "value for %s must be specified" % name_for_errors, location) next_type = next_token[0] next_value = next_token[1] if next_type == token.NAME: result_code = ranges.code_for_symbolic_token(name_for_errors, next_value, location) elif next_type == token.NUMBER: result_code = ranges.code_for_number_token(name_for_errors, next_value, location) elif next_type == token.STRING: result_code = ranges.code_for_string_token(name_for_errors, next_value, location) elif (len(next_value) == 1) and not _tools.is_eof_token(next_token): result_code = ord(next_value) else: raise errors.InterfaceError( 'value for %s must a number, a single character or a symbolic name but is: %s' % (name_for_errors, _compat.text_repr(value)), location) # Ensure there are no further tokens. next_token = next(tokens) if not _tools.is_eof_token(next_token): raise errors.InterfaceError( 'value for %s must be a single character but is: %s' % (name_for_errors, _compat.text_repr(value)), location) # TODO: Handle 'none' properly. assert result_code is not None assert result_code >= 0 result = six.unichr(result_code) assert result is not None return result
def __init__(self, field_name, is_allowed_to_be_empty, length, rule, data_format): super(ChoiceFieldFormat, self).__init__(field_name, is_allowed_to_be_empty, length, rule, data_format, empty_value='') self.choices = [] # Split rule into tokens, ignoring white space. tokens = _tools.tokenize_without_space(rule) # Extract choices from rule tokens. previous_toky = None toky = next(tokens) while not _tools.is_eof_token(toky): if _tools.is_comma_token(toky): # Handle comma after comma without choice. if previous_toky: previous_toky_text = previous_toky[1] else: previous_toky_text = None raise errors.InterfaceError( "choice value must precede a comma (,) but found: %s" % _compat.text_repr(previous_toky_text)) choice = _tools.token_text(toky) if not choice: raise errors.InterfaceError( "choice field must be allowed to be empty instead of containing an empty choice" ) self.choices.append(choice) toky = next(tokens) if not _tools.is_eof_token(toky): if not _tools.is_comma_token(toky): raise errors.InterfaceError( "comma (,) must follow choice value %s but found: %s" % (_compat.text_repr(choice), _compat.text_repr( toky[1]))) # Process next choice after comma. toky = next(tokens) if _tools.is_eof_token(toky): raise errors.InterfaceError( "trailing comma (,) must be removed") if not self.is_allowed_to_be_empty and not self.choices: raise errors.InterfaceError( "choice field without any choices must be allowed to be empty")
def __init__(self, field_name, is_allowed_to_be_empty, length, rule, data_format): super(ChoiceFieldFormat, self).__init__( field_name, is_allowed_to_be_empty, length, rule, data_format, empty_value='') self.choices = [] # Split rule into tokens, ignoring white space. tokens = _tools.tokenize_without_space(rule) # Extract choices from rule tokens. previous_toky = None toky = next(tokens) while not _tools.is_eof_token(toky): if _tools.is_comma_token(toky): # Handle comma after comma without choice. if previous_toky: previous_toky_text = previous_toky[1] else: previous_toky_text = None raise errors.InterfaceError( "choice value must precede a comma (,) but found: %s" % _compat.text_repr(previous_toky_text)) choice = _tools.token_text(toky) if not choice: raise errors.InterfaceError( "choice field must be allowed to be empty instead of containing an empty choice") self.choices.append(choice) toky = next(tokens) if not _tools.is_eof_token(toky): if not _tools.is_comma_token(toky): raise errors.InterfaceError( "comma (,) must follow choice value %s but found: %s" % (_compat.text_repr(choice), _compat.text_repr(toky[1]))) # Process next choice after comma. toky = next(tokens) if _tools.is_eof_token(toky): raise errors.InterfaceError("trailing comma (,) must be removed") if not self.is_allowed_to_be_empty and not self.choices: raise errors.InterfaceError("choice field without any choices must be allowed to be empty")
def __init__(self, description, rule, available_field_names, location=None): super(IsUniqueCheck, self).__init__(description, rule, available_field_names, location) self._field_names_to_check = [] self._row_key_to_location_map = None self.reset() # Extract field names to check from rule. rule_read_line = _compat.token_io_readline(rule) toky = tokenize.generate_tokens(rule_read_line) after_comma = True next_token = next(toky) unique_field_names = set() while (not _tools.is_eof_token(next_token)) and (next_token[0] != tokenize.NEWLINE): token_type = next_token[0] token_value = next_token[1] if after_comma: if token_type != tokenize.NAME: raise errors.InterfaceError( _("field name must contain only ASCII letters, numbers and underscores (_) " "but found: %r [token type=%r]") % (token_value, token_type), self.location_of_rule) try: fields.field_name_index(token_value, available_field_names, location) if token_value in unique_field_names: raise errors.InterfaceError( _("duplicate field name for unique check must be removed: %s" ) % token_value, self.location_of_rule) unique_field_names.add(token_value) except errors.InterfaceError as error: raise errors.InterfaceError(six.text_type(error)) self._field_names_to_check.append(token_value) elif not _tools.is_comma_token(next_token): raise errors.InterfaceError( _("after field name a comma (,) must follow but found: %r") % token_value, self.location_of_rule) after_comma = not after_comma next_token = next(toky) if not len(self._field_names_to_check): raise errors.InterfaceError( _("rule must contain at least one field name to check for uniqueness" ), self.location_of_rule)
def __init__(self, description, rule, available_field_names, location=None): super(IsUniqueCheck, self).__init__(description, rule, available_field_names, location) self._field_names_to_check = [] self._row_key_to_location_map = None self.reset() # Extract field names to check from rule. rule_read_line = _compat.token_io_readline(rule) toky = tokenize.generate_tokens(rule_read_line) after_comma = True next_token = next(toky) unique_field_names = set() while not _tools.is_eof_token(next_token): token_type = next_token[0] token_value = next_token[1] if after_comma: if token_type != tokenize.NAME: raise errors.InterfaceError( "field name must contain only ASCII letters, numbers and underscores (_) " + "but found: %r [token type=%r]" % (token_value, token_type), self.location_of_rule) try: fields.field_name_index(token_value, available_field_names, location) if token_value in unique_field_names: raise errors.InterfaceError( "duplicate field name for unique check must be removed: %s" % token_value, self.location_of_rule) unique_field_names.add(token_value) except errors.InterfaceError as error: raise errors.InterfaceError(six.text_type(error)) self._field_names_to_check.append(token_value) elif not _tools.is_comma_token(next_token): raise errors.InterfaceError( "after field name a comma (,) must follow but found: %r" % token_value, self.location_of_rule) after_comma = not after_comma next_token = next(toky) if not len(self._field_names_to_check): raise errors.InterfaceError( "rule must contain at least one field name to check for uniqueness", self.location_of_rule)
def __init__(self, description, default=None, location=None): """ Setup a decimal range as specified by ``description``. :param str description: a range description of the form \ ``lower...upper`` or ``limit``, possibly consisting of multiple \ items. In case it is empty (``''``), the range specified by \ ``default`` is used; the description also specifies the \ :py:attr:`~cutplace.ranges.DecimalRange.scale` and \ :py:attr:`~cutplace.ranges.DecimalRange.precision` valid numbers \ can use. :param str default: an alternative to use in case ``description`` is ``None`` or empty; in case both ``description`` and \ ``default`` are ``None`` or empty, all values within the \ :py:const:`DEFAULT_SCALE` and :py:const:`DEFAULT_PRECISION` are \ valid. """ assert default is None or (default.strip() != ''), "default=%r" % default self._precision = DEFAULT_PRECISION self._scale = DEFAULT_SCALE # Find out if a `description` has been specified and if not, use optional `default` instead. has_description = (description is not None) and (description.strip() != '') if not has_description and default is not None: description = default has_description = True if not has_description: # Use empty ranges. self._description = None self._items = None self._lower_limit = None self._upper_limit = None else: self._description = description.replace('...', ELLIPSIS) self._items = [] tokens = _tools.tokenize_without_space(self._description) end_reached = False max_digits_after_dot = 0 max_digits_before_dot = 0 while not end_reached: lower = None upper = None ellipsis_found = False after_hyphen = False next_token = next(tokens) while not _tools.is_eof_token( next_token) and not _tools.is_comma_token(next_token): next_type = next_token[0] next_value = next_token[1] if next_type == token.NUMBER: if next_type == token.NUMBER: try: decimal_value = decimal.Decimal(next_value) _sign, digits, exponent = decimal_value.as_tuple( ) digits_after_dot = max(0, -exponent) if digits_after_dot > max_digits_after_dot: max_digits_after_dot = digits_after_dot digits_before_dot = len(digits) + exponent if digits_before_dot > max_digits_before_dot: max_digits_before_dot = digits_before_dot except decimal.DecimalException: raise errors.InterfaceError( _("number must be an decimal or integer but is: %s" ) % _compat.text_repr(next_value), location) if after_hyphen: decimal_value = decimal_value.copy_negate() after_hyphen = False if ellipsis_found: if upper is None: upper = decimal_value else: raise errors.InterfaceError( _("range must have at most lower and upper limit but found another number: %s" ) % _compat.text_repr(next_value), location) elif lower is None: lower = decimal_value else: raise errors.InterfaceError( _("number must be followed by ellipsis (...) but found: %s" ) % _compat.text_repr(next_value)) elif after_hyphen: raise errors.InterfaceError( _("hyphen (-) must be followed by number but found: %s" ) % _compat.text_repr(next_value)) elif (next_type == token.OP) and (next_value == "-"): after_hyphen = True elif next_value in (ELLIPSIS, ':'): ellipsis_found = True else: message = "range must be specified using decimal or integer numbers" \ " and ellipsis (...) but found: %s [token type: %d]" \ % (_compat.text_repr(next_value), next_type) raise errors.InterfaceError(message) next_token = next(tokens) if after_hyphen: raise errors.InterfaceError( _("hyphen (-) at end must be followed by number")) # Decide upon the result. if lower is None: if upper is None: if ellipsis_found: # Handle "...". # TODO: Handle "..." same as ""? raise errors.InterfaceError( _("ellipsis (...) must be preceded and/or succeeded by number" )) else: assert ellipsis_found # Handle "...y". range_item = (None, upper) elif ellipsis_found: # Handle "x..." and "x...y". if (upper is not None) and (lower > upper): raise errors.InterfaceError( _("lower limit %s must be less or equal than upper limit %s" ) % (_decimal_as_text(lower, self.precision), _decimal_as_text(upper, self.precision))) range_item = (lower, upper) else: # Handle "x". range_item = (lower, lower) if range_item is not None: self._precision = max_digits_after_dot self._scale = max_digits_before_dot + max_digits_after_dot for item in self._items: if self._items_overlap(item, range_item): item_text = _compat.text_repr( self._repr_item(item)) result_text = _compat.text_repr( self._repr_item(range_item)) raise errors.InterfaceError( _("overlapping parts in decimal range must be cleaned up: %s and %s" ) % (item_text, result_text), location) self._items.append(range_item) if _tools.is_eof_token(next_token): end_reached = True assert self.precision >= 0 assert self.scale >= self.precision self._lower_limit = None self._upper_limit = None is_first_item = True for lower_item, upper_item in self._items: if is_first_item: self._lower_limit = lower_item self._upper_limit = upper_item is_first_item = False if lower_item is None: self._lower_limit = None elif (self._lower_limit is not None) and (lower_item < self._lower_limit): self._lower_limit = lower_item if upper_item is None: self._upper_limit = None elif (self._upper_limit is not None) and (upper_item > self._upper_limit): self._upper_limit = upper_item
def __init__(self, description, default=None): """ Setup a range as specified by ``description``. :param str description: a range description of the form \ ``lower...upper`` or ``limit``. In case it is empty (``''``), any \ value will be accepted by \ :py:meth:`~cutplace.ranges.Range.validate()`. For example, \ ``1...40`` accepts values between 1 and 40. :param str default: an alternative to use in case ``description`` is \ ``None`` or empty. """ assert default is None or (default.strip() != ''), "default=%r" % default # Find out if a `description` has been specified and if not, use optional `default` instead. has_description = (description is not None) and (description.strip() != '') if not has_description and default is not None: description = default has_description = True if not has_description: # Use empty ranges. self._description = None self._items = None self._lower_limit = None self._upper_limit = None else: self._description = description.replace('...', ELLIPSIS) self._items = [] name_for_code = 'range' location = None # TODO: Add location where range is declared. tokens = _tools.tokenize_without_space(self._description) end_reached = False while not end_reached: lower = None upper = None ellipsis_found = False after_hyphen = False next_token = next(tokens) while not _tools.is_eof_token( next_token) and not _tools.is_comma_token(next_token): next_type = next_token[0] next_value = next_token[1] if next_type in (token.NAME, token.NUMBER, token.STRING): if next_type == token.NAME: # Symbolic names, e.g. ``tab``. value_as_int = code_for_symbolic_token( name_for_code, next_value, location) elif next_type == token.NUMBER: # Numbers, e.g. ``123``. value_as_int = code_for_number_token( name_for_code, next_value, location) if after_hyphen: value_as_int *= -1 after_hyphen = False elif next_type == token.STRING: # Python strings, e.g. ``'abc'`` or ``"""abc"""``. value_as_int = code_for_string_token( name_for_code, next_value, location) elif (len(next_value) == 1) and not _tools.is_eof_token(next_token): # Other single characters, e.g. ``,``; this is particular useful with delimiter properties. value_as_int = ord(next_value) else: raise errors.InterfaceError( _('value for %s must a number, a single character or a symbolic name but is: %s' ) % (name_for_code, _compat.text_repr(next_value)), location) if ellipsis_found: if upper is None: upper = value_as_int else: raise errors.InterfaceError( _("range must have at most lower and upper limit but found another number: %s" ) % _compat.text_repr(next_value), location) elif lower is None: lower = value_as_int else: raise errors.InterfaceError( _("number must be followed by ellipsis (...) but found: %s" ) % _compat.text_repr(next_value), location) elif after_hyphen: raise errors.InterfaceError( _("hyphen (-) must be followed by number but found: %s" ) % _compat.text_repr(next_value), location) elif (next_type == token.OP) and (next_value == "-"): after_hyphen = True elif next_value in (ELLIPSIS, ':'): ellipsis_found = True else: raise errors.InterfaceError( _("range must be specified using integer numbers, text, " "symbols and ellipsis (...) but found: %s [token type: %d]" ) % (_compat.text_repr(next_value), next_type), location) next_token = next(tokens) if after_hyphen: raise errors.InterfaceError( _("hyphen (-) at end must be followed by number"), location) # Decide upon the result. if lower is None: if upper is None: if ellipsis_found: # Handle "...". raise errors.InterfaceError( _('ellipsis (...) must be preceded and/or succeeded by number' ), location) else: # Handle "". result = None else: assert ellipsis_found # Handle "...y". result = (None, upper) elif ellipsis_found: # Handle "x..." and "x...y". if (upper is not None) and (lower > upper): raise errors.InterfaceError( _("lower range %d must be greater or equal than upper range %d" ) % (lower, upper), location) result = (lower, upper) else: # Handle "x". result = (lower, lower) if result is not None: for item in self._items: if self._items_overlap(item, result): item_text = _compat.text_repr( self._repr_item(item)) result_text = _compat.text_repr( self._repr_item(result)) raise errors.InterfaceError( _("overlapping parts in range must be cleaned up: %s and %s" ) % (item_text, result_text), location) self._items.append(result) if _tools.is_eof_token(next_token): end_reached = True self._lower_limit = None self._upper_limit = None is_first_item = True for lower_item, upper_item in self._items: if is_first_item: self._lower_limit = lower_item self._upper_limit = upper_item is_first_item = False if lower_item is None: self._lower_limit = None elif (self._lower_limit is not None) and (lower_item < self._lower_limit): self._lower_limit = lower_item if upper_item is None: self._upper_limit = None elif (self._upper_limit is not None) and (upper_item > self._upper_limit): self._upper_limit = upper_item
def __init__(self, description, default=None, location=None): """ Setup a decimal range as specified by ``description``. :param str description: a range description of the form \ ``lower...upper`` or ``limit``, possibly consisting of multiple \ items. In case it is empty (``''``), the range specified by \ ``default`` is used; the description also specifies the \ :py:attr:`~cutplace.ranges.DecimalRange.scale` and \ :py:attr:`~cutplace.ranges.DecimalRange.precision` valid numbers \ can use. :param str default: an alternative to use in case ``description`` is ``None`` or empty; in case both ``description`` and \ ``default`` are ``None`` or empty, all values within the \ :py:const:`DEFAULT_SCALE` and :py:const:`DEFAULT_PRECISION` are \ valid. """ assert default is None or (default.strip() != ''), "default=%r" % default self._precision = DEFAULT_PRECISION self._scale = DEFAULT_SCALE # Find out if a `description` has been specified and if not, use optional `default` instead. has_description = (description is not None) and (description.strip() != '') if not has_description and default is not None: description = default has_description = True if not has_description: # Use empty ranges. self._description = None self._items = None self._lower_limit = None self._upper_limit = None else: self._description = description.replace('...', ELLIPSIS) self._items = [] tokens = _tools.tokenize_without_space(self._description) end_reached = False max_digits_after_dot = 0 max_digits_before_dot = 0 while not end_reached: lower = None upper = None ellipsis_found = False after_hyphen = False next_token = next(tokens) while not _tools.is_eof_token(next_token) and not _tools.is_comma_token(next_token): next_type = next_token[0] next_value = next_token[1] if next_type == token.NUMBER: if next_type == token.NUMBER: try: decimal_value = decimal.Decimal(next_value) _, digits, exponent = decimal_value.as_tuple() digits_after_dot = max(0, -exponent) if digits_after_dot > max_digits_after_dot: max_digits_after_dot = digits_after_dot digits_before_dot = len(digits) + exponent if digits_before_dot > max_digits_before_dot: max_digits_before_dot = digits_before_dot except decimal.DecimalException: raise errors.InterfaceError( "number must be an decimal or integer but is: %s" % _compat.text_repr(next_value), location) if after_hyphen: decimal_value = decimal_value.copy_negate() after_hyphen = False if ellipsis_found: if upper is None: upper = decimal_value else: raise errors.InterfaceError( "range must have at most lower and upper limit but found another number: %s" % _compat.text_repr(next_value), location) elif lower is None: lower = decimal_value else: raise errors.InterfaceError( "number must be followed by ellipsis (...) but found: %s" % _compat.text_repr(next_value)) elif after_hyphen: raise errors.InterfaceError( "hyphen (-) must be followed by number but found: %s" % _compat.text_repr(next_value)) elif (next_type == token.OP) and (next_value == "-"): after_hyphen = True elif next_value in (ELLIPSIS, ':'): ellipsis_found = True else: message = "range must be specified using decimal or integer numbers" \ " and ellipsis (...) but found: %s [token type: %d]" \ % (_compat.text_repr(next_value), next_type) raise errors.InterfaceError(message) next_token = next(tokens) if after_hyphen: raise errors.InterfaceError("hyphen (-) at end must be followed by number") # Decide upon the result. if lower is None: if upper is None: if ellipsis_found: # Handle "...". # TODO: Handle "..." same as ""? raise errors.InterfaceError("ellipsis (...) must be preceded and/or succeeded by number") else: assert ellipsis_found # Handle "...y". range_item = (None, upper) elif ellipsis_found: # Handle "x..." and "x...y". if (upper is not None) and (lower > upper): raise errors.InterfaceError( "lower limit %s must be less or equal than upper limit %s" % (_decimal_as_text(lower, self.precision), _decimal_as_text(upper, self.precision))) range_item = (lower, upper) else: # Handle "x". range_item = (lower, lower) if range_item is not None: self._precision = max_digits_after_dot self._scale = max_digits_before_dot + max_digits_after_dot for item in self._items: if self._items_overlap(item, range_item): item_text = _compat.text_repr(self._repr_item(item)) result_text = _compat.text_repr(self._repr_item(range_item)) raise errors.InterfaceError( "overlapping parts in decimal range must be cleaned up: %s and %s" % (item_text, result_text), location) self._items.append(range_item) if _tools.is_eof_token(next_token): end_reached = True assert self.precision >= 0 assert self.scale >= self.precision self._lower_limit = None self._upper_limit = None is_first_item = True for lower_item, upper_item in self._items: if is_first_item: self._lower_limit = lower_item self._upper_limit = upper_item is_first_item = False if lower_item is None: self._lower_limit = None elif (self._lower_limit is not None) and (lower_item < self._lower_limit): self._lower_limit = lower_item if upper_item is None: self._upper_limit = None elif (self._upper_limit is not None) and (upper_item > self._upper_limit): self._upper_limit = upper_item
def __init__(self, description, default=None): """ Setup a range as specified by ``description``. :param str description: a range description of the form \ ``lower...upper`` or ``limit``. In case it is empty (``''``), any \ value will be accepted by \ :py:meth:`~cutplace.ranges.Range.validate()`. For example, \ ``1...40`` accepts values between 1 and 40. :param str default: an alternative to use in case ``description`` is \ ``None`` or empty. """ assert default is None or (default.strip() != ''), "default=%r" % default # Find out if a `description` has been specified and if not, use optional `default` instead. has_description = (description is not None) and (description.strip() != '') if not has_description and default is not None: description = default has_description = True if not has_description: # Use empty ranges. self._description = None self._items = None self._lower_limit = None self._upper_limit = None else: self._description = description.replace('...', ELLIPSIS) self._items = [] name_for_code = 'range' location = None # TODO: Add location where range is declared. tokens = _tools.tokenize_without_space(self._description) end_reached = False while not end_reached: lower = None upper = None ellipsis_found = False after_hyphen = False next_token = next(tokens) while not _tools.is_eof_token(next_token) and not _tools.is_comma_token(next_token): next_type = next_token[0] next_value = next_token[1] if next_type in (token.NAME, token.NUMBER, token.STRING): if next_type == token.NAME: # Symbolic names, e.g. ``tab``. value_as_int = code_for_symbolic_token(name_for_code, next_value, location) elif next_type == token.NUMBER: # Numbers, e.g. ``123``. value_as_int = code_for_number_token(name_for_code, next_value, location) if after_hyphen: value_as_int *= - 1 after_hyphen = False elif next_type == token.STRING: # Python strings, e.g. ``'abc'`` or ``"""abc"""``. value_as_int = code_for_string_token(name_for_code, next_value, location) elif (len(next_value) == 1) and not _tools.is_eof_token(next_token): # Other single characters, e.g. ``,``; this is particular useful with delimiter properties. value_as_int = ord(next_value) else: raise errors.InterfaceError( 'value for %s must a number, a single character or a symbolic name but is: %s' % (name_for_code, _compat.text_repr(next_value)), location) if ellipsis_found: if upper is None: upper = value_as_int else: raise errors.InterfaceError( "range must have at most lower and upper limit but found another number: %s" % _compat.text_repr(next_value), location) elif lower is None: lower = value_as_int else: raise errors.InterfaceError( "number must be followed by ellipsis (...) but found: %s" % _compat.text_repr(next_value), location) elif after_hyphen: raise errors.InterfaceError( "hyphen (-) must be followed by number but found: %s" % _compat.text_repr(next_value), location) elif (next_type == token.OP) and (next_value == "-"): after_hyphen = True elif next_value in (ELLIPSIS, ':'): ellipsis_found = True else: raise errors.InterfaceError( "range must be specified using integer numbers, text, " "symbols and ellipsis (...) but found: %s [token type: %d]" % (_compat.text_repr(next_value), next_type), location) next_token = next(tokens) if after_hyphen: raise errors.InterfaceError("hyphen (-) at end must be followed by number", location) # Decide upon the result. if lower is None: if upper is None: if ellipsis_found: # Handle "...". raise errors.InterfaceError( 'ellipsis (...) must be preceded and/or succeeded by number', location) else: # Handle "". result = None else: assert ellipsis_found # Handle "...y". result = (None, upper) elif ellipsis_found: # Handle "x..." and "x...y". if (upper is not None) and (lower > upper): raise errors.InterfaceError( "lower range %d must be greater or equal than upper range %d" % (lower, upper), location) result = (lower, upper) else: # Handle "x". result = (lower, lower) if result is not None: for item in self._items: if self._items_overlap(item, result): item_text = _compat.text_repr(self._repr_item(item)) result_text = _compat.text_repr(self._repr_item(result)) raise errors.InterfaceError( "overlapping parts in range must be cleaned up: %s and %s" % (item_text, result_text), location) self._items.append(result) if _tools.is_eof_token(next_token): end_reached = True self._lower_limit = None self._upper_limit = None is_first_item = True for lower_item, upper_item in self._items: if is_first_item: self._lower_limit = lower_item self._upper_limit = upper_item is_first_item = False if lower_item is None: self._lower_limit = None elif (self._lower_limit is not None) and (lower_item < self._lower_limit): self._lower_limit = lower_item if upper_item is None: self._upper_limit = None elif (self._upper_limit is not None) and (upper_item > self._upper_limit): self._upper_limit = upper_item