1# -*- coding: utf-8 -*- 2# pylint: disable=missing-docstring 3""" 4Provides generic utility classes for the :class:`parse.Parser` class. 5""" 6 7from __future__ import absolute_import 8from collections import namedtuple 9import parse 10import six 11 12 13# -- HELPER-CLASS: For format part in a Field. 14# REQUIRES: Python 2.6 or newer. 15# pylint: disable=redefined-builtin, too-many-arguments 16FormatSpec = namedtuple("FormatSpec", 17 ["type", "width", "zero", "align", "fill", "precision"]) 18 19def make_format_spec(type=None, width="", zero=False, align=None, fill=None, 20 precision=None): 21 return FormatSpec(type, width, zero, align, fill, precision) 22# pylint: enable=redefined-builtin 23 24class Field(object): 25 """ 26 Provides a ValueObject for a Field in a parse expression. 27 28 Examples: 29 * "{}" 30 * "{name}" 31 * "{:format}" 32 * "{name:format}" 33 34 Format specification: [[fill]align][0][width][.precision][type] 35 """ 36 # pylint: disable=redefined-builtin 37 ALIGN_CHARS = '<>=^' 38 39 def __init__(self, name="", format=None): 40 self.name = name 41 self.format = format 42 self._format_spec = None 43 44 def set_format(self, format): 45 self.format = format 46 self._format_spec = None 47 48 @property 49 def has_format(self): 50 return bool(self.format) 51 52 @property 53 def format_spec(self): 54 if not self._format_spec and self.format: 55 self._format_spec = self.extract_format_spec(self.format) 56 return self._format_spec 57 58 def __str__(self): 59 name = self.name or "" 60 if self.has_format: 61 return "{%s:%s}" % (name, self.format) 62 return "{%s}" % name 63 64 def __eq__(self, other): 65 if isinstance(other, Field): 66 format1 = self.format or "" 67 format2 = other.format or "" 68 return (self.name == other.name) and (format1 == format2) 69 elif isinstance(other, six.string_types): 70 return str(self) == other 71 else: 72 raise ValueError(other) 73 74 def __ne__(self, other): 75 return not self.__eq__(other) 76 77 @staticmethod 78 def make_format(format_spec): 79 """Build format string from a format specification. 80 81 :param format_spec: Format specification (as FormatSpec object). 82 :return: Composed format (as string). 83 """ 84 fill = '' 85 align = '' 86 zero = '' 87 width = format_spec.width 88 if format_spec.align: 89 align = format_spec.align[0] 90 if format_spec.fill: 91 fill = format_spec.fill[0] 92 if format_spec.zero: 93 zero = '0' 94 95 precision_part = "" 96 if format_spec.precision: 97 precision_part = ".%s" % format_spec.precision 98 99 # -- FORMAT-SPEC: [[fill]align][0][width][.precision][type] 100 return "%s%s%s%s%s%s" % (fill, align, zero, width, 101 precision_part, format_spec.type) 102 103 104 @classmethod 105 def extract_format_spec(cls, format): 106 """Pull apart the format: [[fill]align][0][width][.precision][type]""" 107 # -- BASED-ON: parse.extract_format() 108 # pylint: disable=redefined-builtin, unsubscriptable-object 109 if not format: 110 raise ValueError("INVALID-FORMAT: %s (empty-string)" % format) 111 112 orig_format = format 113 fill = align = None 114 if format[0] in cls.ALIGN_CHARS: 115 align = format[0] 116 format = format[1:] 117 elif len(format) > 1 and format[1] in cls.ALIGN_CHARS: 118 fill = format[0] 119 align = format[1] 120 format = format[2:] 121 122 zero = False 123 if format and format[0] == '0': 124 zero = True 125 format = format[1:] 126 127 width = '' 128 while format: 129 if not format[0].isdigit(): 130 break 131 width += format[0] 132 format = format[1:] 133 134 precision = None 135 if format.startswith('.'): 136 # Precision isn't needed but we need to capture it so that 137 # the ValueError isn't raised. 138 format = format[1:] # drop the '.' 139 precision = '' 140 while format: 141 if not format[0].isdigit(): 142 break 143 precision += format[0] 144 format = format[1:] 145 146 # the rest is the type, if present 147 type = format 148 if not type: 149 raise ValueError("INVALID-FORMAT: %s (without type)" % orig_format) 150 return FormatSpec(type, width, zero, align, fill, precision) 151 152 153class FieldParser(object): 154 """ 155 Utility class that parses/extracts fields in parse expressions. 156 """ 157 158 @classmethod 159 def parse(cls, text): 160 if not (text.startswith('{') and text.endswith('}')): 161 message = "FIELD-SCHEMA MISMATCH: text='%s' (missing braces)" % text 162 raise ValueError(message) 163 164 # first: lose the braces 165 text = text[1:-1] 166 if ':' in text: 167 # -- CASE: Typed field with format. 168 name, format_ = text.split(':') 169 else: 170 name = text 171 format_ = None 172 return Field(name, format_) 173 174 @classmethod 175 def extract_fields(cls, schema): 176 """Extract fields in a parse expression schema. 177 178 :param schema: Parse expression schema/format to use (as string). 179 :return: Generator for fields in schema (as Field objects). 180 """ 181 # -- BASED-ON: parse.Parser._generate_expression() 182 for part in parse.PARSE_RE.split(schema): 183 if not part or part == '{{' or part == '}}': 184 continue 185 elif part[0] == '{': 186 # this will be a braces-delimited field to handle 187 yield cls.parse(part) 188 189 @classmethod 190 def extract_types(cls, schema): 191 """Extract types (names) for typed fields (with format/type part). 192 193 :param schema: Parser schema/format to use. 194 :return: Generator for type names (as string). 195 """ 196 for field in cls.extract_fields(schema): 197 if field.has_format: 198 yield field.format_spec.type 199