codekingpro/portable-devtools
115k
1"""2PYTHON SOFTWARE FOUNDATION LICENSE VERSION 23--------------------------------------------4 51. This LICENSE AGREEMENT is between the Python Software Foundation6("PSF"), and the Individual or Organization ("Licensee") accessing and7otherwise using this software ("Python") in source or binary form and8its associated documentation.9 102. Subject to the terms and conditions of this License Agreement, PSF hereby11grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,12analyze, test, perform and/or display publicly, prepare derivative works,13distribute, and otherwise use Python alone or in any derivative version,14provided, however, that PSF's License Agreement and PSF's notice of copyright,15i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,2010162011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights17Reserved" are retained in Python alone or in any derivative version prepared by18Licensee.19 203. In the event Licensee prepares a derivative work that is based on21or incorporates Python or any part thereof, and wants to make22the derivative work available to others as provided herein, then23Licensee hereby agrees to include in any such work a brief summary of24the changes made to Python.25 264. PSF is making Python available to Licensee on an "AS IS"27basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR28IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND29DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS30FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT31INFRINGE ANY THIRD PARTY RIGHTS.32 335. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON34FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS35A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,36OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.37 386. This License Agreement will automatically terminate upon a material39breach of its terms and conditions.40 417. Nothing in this License Agreement shall be deemed to create any42relationship of agency, partnership, or joint venture between PSF and43Licensee. This License Agreement does not grant permission to use PSF44trademarks or trade name in a trademark sense to endorse or promote45products or services of Licensee, or any third party.46 478. By copying, installing or otherwise using Python, Licensee48agrees to be bound by the terms and conditions of this License49Agreement.50"""51 52############################################################################53# Changes:54# Added new parameter in dialect 'replace_nulls_with' to compare it against55# the value to be quoted or not.56# Handle the null value if value is None or equal to57# 'replace_nulls_with' then it represents the null value, so no need to58# quote it.59############################################################################60 61__all__ = ["QUOTE_MINIMAL", "QUOTE_ALL", "QUOTE_NONNUMERIC", "QUOTE_NONE",62 "Error", "Dialect", "__doc__", "Excel", "ExcelTab",63 "field_size_limit", "Reader", "Writer", "register_dialect",64 "get_dialect", "list_dialects", "unregister_dialect",65 "__version__", "DictReader", "DictWriter"]66 67import re68import numbers69from io import StringIO70from csv import (71 QUOTE_MINIMAL, QUOTE_ALL, QUOTE_NONNUMERIC, QUOTE_NONE,72 __version__, __doc__, Error, field_size_limit,73)74 75 76class QuoteStrategy():77 quoting = None78 79 def __init__(self, dialect):80 if self.quoting is not None:81 assert dialect.quoting == self.quoting82 self.dialect = dialect83 self.setup()84 85 escape_pattern_quoted = r'({quotechar})'.format(86 quotechar=re.escape(self.dialect.quotechar or '"'))87 escape_pattern_unquoted = r'([{specialchars}])'.format(88 specialchars=re.escape(self.specialchars))89 90 self.escape_re_quoted = re.compile(escape_pattern_quoted)91 self.escape_re_unquoted = re.compile(escape_pattern_unquoted)92 93 def setup(self):94 """Optional method for strategy-wide optimizations."""95 96 def quoted(self, field=None, raw_field=None, only=None):97 """Determine whether this field should be quoted."""98 raise NotImplementedError(99 'quoted must be implemented by a subclass')100 101 @property102 def specialchars(self):103 """The special characters that need to be escaped."""104 raise NotImplementedError(105 'specialchars must be implemented by a subclass')106 107 def escape_re(self, quoted=None):108 if quoted:109 return self.escape_re_quoted110 return self.escape_re_unquoted111 112 def escapechar(self, quoted=None):113 if quoted and self.dialect.doublequote:114 return self.dialect.quotechar115 return self.dialect.escapechar116 117 def prepare(self, raw_field, only=None):118 field = str(raw_field if raw_field is not None else '')119 quoted = self.quoted(field=field, raw_field=raw_field, only=only)120 121 escape_re = self.escape_re(quoted=quoted)122 escapechar = self.escapechar(quoted=quoted)123 124 if escape_re.search(field):125 escapechar = '\\\\' if escapechar == '\\' else escapechar126 if escapechar:127 escape_replace = \128 r'{escapechar}\1'.format(escapechar=escapechar)129 field = escape_re.sub(escape_replace, field)130 131 if quoted:132 field = '{quotechar}{field}{quotechar}'.format(133 quotechar=self.dialect.quotechar, field=field)134 135 return field136 137 138class QuoteMinimalStrategy(QuoteStrategy):139 quoting = QUOTE_MINIMAL140 141 def setup(self):142 self.quoted_re = re.compile(r'[{specialchars}]'.format(143 specialchars=re.escape(self.specialchars)))144 145 @property146 def specialchars(self):147 return (148 self.dialect.lineterminator +149 self.dialect.quotechar +150 self.dialect.delimiter +151 (self.dialect.escapechar or '')152 )153 154 def quoted(self, field, only, **kwargs):155 if field == self.dialect.quotechar and not self.dialect.doublequote:156 # If the only character in the field is the quotechar, and157 # doublequote is false, then just escape without outer quotes.158 return False159 return field == '' and only or bool(self.quoted_re.search(field))160 161 162class QuoteAllStrategy(QuoteStrategy):163 quoting = QUOTE_ALL164 165 @property166 def specialchars(self):167 return self.dialect.quotechar168 169 def quoted(self, raw_field, **kwargs):170 # Handle the null value if raw_field is None or equal to171 # replace_nulls_with then it represents the null value, so no need to172 # quote it.173 if raw_field is None or raw_field == self.dialect.replace_nulls_with:174 return False175 return True176 177 178class QuoteNonnumericStrategy(QuoteStrategy):179 quoting = QUOTE_NONNUMERIC180 181 @property182 def specialchars(self):183 return (184 self.dialect.lineterminator +185 self.dialect.quotechar +186 self.dialect.delimiter +187 (self.dialect.escapechar or '')188 )189 190 def quoted(self, raw_field, **kwargs):191 # Handle the null value if raw_field is None or equal to192 # replace_nulls_with then it represents the null value, so no need to193 # quote it.194 if raw_field is None or raw_field == self.dialect.replace_nulls_with:195 return False196 return not isinstance(raw_field, numbers.Number)197 198 199class QuoteNoneStrategy(QuoteStrategy):200 quoting = QUOTE_NONE201 202 @property203 def specialchars(self):204 return (205 self.dialect.lineterminator +206 (self.dialect.quotechar or '') +207 self.dialect.delimiter +208 (self.dialect.escapechar or '')209 )210 211 def quoted(self, field, only, **kwargs):212 if field == '' and only:213 raise Error('single empty field record must be quoted')214 return False215 216 217class Writer():218 def __init__(self, fileobj, dialect='excel', **fmtparams):219 if fileobj is None:220 raise TypeError('fileobj must be file-like, not None')221 222 self.fileobj = fileobj223 224 if isinstance(dialect, str):225 dialect = get_dialect(dialect)226 227 try:228 self.dialect = Dialect.combine(dialect, fmtparams)229 except Error as e:230 raise TypeError(*e.args)231 232 strategies = {233 QUOTE_MINIMAL: QuoteMinimalStrategy,234 QUOTE_ALL: QuoteAllStrategy,235 QUOTE_NONNUMERIC: QuoteNonnumericStrategy,236 QUOTE_NONE: QuoteNoneStrategy,237 }238 self.strategy = strategies[self.dialect.quoting](self.dialect)239 240 def writerow(self, row):241 if row is None:242 raise Error('row must be an iterable')243 244 row = list(row)245 only = len(row) == 1246 row = [self.strategy.prepare(field, only=only) for field in row]247 248 line = self.dialect.delimiter.join(row) + self.dialect.lineterminator249 return self.fileobj.write(line)250 251 def writerows(self, rows):252 for row in rows:253 self.writerow(row)254 255 256START_RECORD = 0257START_FIELD = 1258ESCAPED_CHAR = 2259IN_FIELD = 3260IN_QUOTED_FIELD = 4261ESCAPE_IN_QUOTED_FIELD = 5262QUOTE_IN_QUOTED_FIELD = 6263EAT_CRNL = 7264AFTER_ESCAPED_CRNL = 8265 266 267class Reader():268 def __init__(self, fileobj, dialect='excel', **fmtparams):269 self.input_iter = iter(fileobj)270 271 if isinstance(dialect, str):272 dialect = get_dialect(dialect)273 274 try:275 self.dialect = Dialect.combine(dialect, fmtparams)276 except Error as e:277 raise TypeError(*e.args)278 279 self.fields = None280 self.field = None281 self.line_num = 0282 283 def parse_reset(self):284 self.fields = []285 self.field = []286 self.state = START_RECORD287 self.numeric_field = False288 289 def parse_save_field(self):290 field = ''.join(self.field)291 self.field = []292 if self.numeric_field:293 field = float(field)294 self.numeric_field = False295 self.fields.append(field)296 297 def parse_add_char(self, c):298 if len(self.field) >= field_size_limit():299 raise Error('field size limit exceeded')300 self.field.append(c)301 302 def parse_process_char(self, c):303 switch = {304 START_RECORD: self._parse_start_record,305 START_FIELD: self._parse_start_field,306 ESCAPED_CHAR: self._parse_escaped_char,307 AFTER_ESCAPED_CRNL: self._parse_after_escaped_crnl,308 IN_FIELD: self._parse_in_field,309 IN_QUOTED_FIELD: self._parse_in_quoted_field,310 ESCAPE_IN_QUOTED_FIELD: self._parse_escape_in_quoted_field,311 QUOTE_IN_QUOTED_FIELD: self._parse_quote_in_quoted_field,312 EAT_CRNL: self._parse_eat_crnl,313 }314 return switch[self.state](c)315 316 def _parse_start_record(self, c):317 if c == '\0':318 return319 elif c == '\n' or c == '\r':320 self.state = EAT_CRNL321 return322 323 self.state = START_FIELD324 return self._parse_start_field(c)325 326 def _parse_start_field(self, c):327 if c == '\n' or c == '\r' or c == '\0':328 self.parse_save_field()329 self.state = START_RECORD if c == '\0' else EAT_CRNL330 elif (c == self.dialect.quotechar and331 self.dialect.quoting != QUOTE_NONE):332 self.state = IN_QUOTED_FIELD333 elif c == self.dialect.escapechar:334 self.state = ESCAPED_CHAR335 elif c == ' ' and self.dialect.skipinitialspace:336 pass # Ignore space at start of field337 elif c == self.dialect.delimiter:338 # Save empty field339 self.parse_save_field()340 else:341 # Begin new unquoted field342 if self.dialect.quoting == QUOTE_NONNUMERIC:343 self.numeric_field = True344 self.parse_add_char(c)345 self.state = IN_FIELD346 347 def _parse_escaped_char(self, c):348 if c == '\n' or c == '\r':349 self.parse_add_char(c)350 self.state = AFTER_ESCAPED_CRNL351 return352 if c == '\0':353 c = '\n'354 self.parse_add_char(c)355 self.state = IN_FIELD356 357 def _parse_after_escaped_crnl(self, c):358 if c == '\0':359 return360 return self._parse_in_field(c)361 362 def _parse_in_field(self, c):363 # In unquoted field364 if c == '\n' or c == '\r' or c == '\0':365 # End of line - return [fields]366 self.parse_save_field()367 self.state = START_RECORD if c == '\0' else EAT_CRNL368 elif c == self.dialect.escapechar:369 self.state = ESCAPED_CHAR370 elif c == self.dialect.delimiter:371 self.parse_save_field()372 self.state = START_FIELD373 else:374 # Normal character - save in field375 self.parse_add_char(c)376 377 def _parse_in_quoted_field(self, c):378 if c != '\0' and c == self.dialect.escapechar:379 self.state = ESCAPE_IN_QUOTED_FIELD380 elif c != '\0' and (c == self.dialect.quotechar and381 self.dialect.quoting != QUOTE_NONE):382 if self.dialect.doublequote:383 self.state = QUOTE_IN_QUOTED_FIELD384 else:385 self.state = IN_FIELD386 elif c != '\0':387 self.parse_add_char(c)388 389 def _parse_escape_in_quoted_field(self, c):390 if c == '\0':391 c = '\n'392 393 self.parse_add_char(c)394 self.state = IN_QUOTED_FIELD395 396 def _parse_quote_in_quoted_field(self, c):397 if (self.dialect.quoting != QUOTE_NONE and398 c == self.dialect.quotechar):399 # save "" as "400 self.parse_add_char(c)401 self.state = IN_QUOTED_FIELD402 elif c == self.dialect.delimiter:403 self.parse_save_field()404 self.state = START_FIELD405 elif c == '\n' or c == '\r' or c == '\0':406 # End of line = return [fields]407 self.parse_save_field()408 self.state = START_RECORD if c == '\0' else EAT_CRNL409 elif not self.dialect.strict:410 self.parse_add_char(c)411 self.state = IN_FIELD412 else:413 # illegal414 raise Error("{delimiter}' expected after '{quotechar}".format(415 delimiter=self.dialect.delimiter,416 quotechar=self.dialect.quotechar,417 ))418 419 def _parse_eat_crnl(self, c):420 if c != '\n' and c != '\r' and c == '\0':421 self.state = START_RECORD422 elif c != '\n' and c != '\r':423 raise Error('new-line character seen in unquoted field - do you '424 'need to open the file in universal-newline mode?')425 426 def __iter__(self):427 return self428 429 def __next__(self):430 self.parse_reset()431 432 while True:433 try:434 lineobj = next(self.input_iter)435 except StopIteration:436 if len(self.field) != 0 or self.state == IN_QUOTED_FIELD:437 if self.dialect.strict:438 raise Error('unexpected end of data')439 self.parse_save_field()440 if self.fields:441 break442 raise443 444 if not isinstance(lineobj, str):445 typ = type(lineobj)446 typ_name = 'bytes' if typ == bytes else typ.__name__447 err_str = ('iterator should return strings, not {0}'448 ' (did you open the file in text mode?)')449 raise Error(err_str.format(typ_name))450 451 self.line_num += 1452 for c in lineobj:453 if c == '\0':454 raise Error('line contains NULL byte')455 self.parse_process_char(c)456 457 self.parse_process_char('\0')458 459 if self.state == START_RECORD:460 break461 462 fields = self.fields463 self.fields = None464 return fields465 466 next = __next__467 468 469_dialect_registry = {}470 471 472def register_dialect(name, dialect='excel', **fmtparams):473 if not isinstance(name, str):474 raise TypeError('"name" must be a string')475 476 dialect = Dialect.extend(dialect, fmtparams)477 478 try:479 Dialect.validate(dialect)480 except Exception:481 raise TypeError('dialect is invalid')482 483 assert name not in _dialect_registry484 _dialect_registry[name] = dialect485 486 487def unregister_dialect(name):488 try:489 _dialect_registry.pop(name)490 except KeyError:491 raise Error('"{name}" not a registered dialect'.format(name=name))492 493 494def get_dialect(name):495 try:496 return _dialect_registry[name]497 except KeyError:498 raise Error('Could not find dialect {0}'.format(name))499 500 501def list_dialects():502 return list(_dialect_registry)503 504 505class Dialect():506 """Describe a CSV dialect.507 This must be subclassed (see csv.excel). Valid attributes are:508 delimiter, quotechar, escapechar, doublequote, skipinitialspace,509 lineterminator, quoting, strict.510 """511 _name = ""512 _valid = False513 # placeholders514 delimiter = None515 quotechar = None516 escapechar = None517 doublequote = None518 skipinitialspace = None519 lineterminator = None520 quoting = None521 strict = None522 523 def __init__(self):524 self.validate(self)525 if self.__class__ != Dialect:526 self._valid = True527 528 @classmethod529 def validate(cls, dialect):530 dialect = cls.extend(dialect)531 532 if not isinstance(dialect.quoting, int):533 raise Error('"quoting" must be an integer')534 535 if dialect.delimiter is None:536 raise Error('delimiter must be set')537 cls.validate_text(dialect, 'delimiter')538 539 if dialect.lineterminator is None:540 raise Error('lineterminator must be set')541 if not isinstance(dialect.lineterminator, str):542 raise Error('"lineterminator" must be a string')543 544 if dialect.quoting not in [545 QUOTE_NONE, QUOTE_MINIMAL, QUOTE_NONNUMERIC, QUOTE_ALL]:546 raise Error('Invalid quoting specified')547 548 if dialect.quoting != QUOTE_NONE:549 if dialect.quotechar is None and dialect.escapechar is None:550 raise Error('quotechar must be set if quoting enabled')551 if dialect.quotechar is not None:552 cls.validate_text(dialect, 'quotechar')553 554 @staticmethod555 def validate_text(dialect, attr):556 val = getattr(dialect, attr)557 if not isinstance(val, str):558 if isinstance(val, bytes):559 raise Error('"{0}" must be string, not bytes'.format(attr))560 raise Error('"{0}" must be string, not {1}'.format(561 attr, type(val).__name__))562 563 if len(val) != 1:564 raise Error('"{0}" must be a 1-character string'.format(attr))565 566 @staticmethod567 def defaults():568 return {569 'delimiter': ',',570 'doublequote': True,571 'escapechar': None,572 'lineterminator': '\r\n',573 'quotechar': '"',574 'quoting': QUOTE_MINIMAL,575 'skipinitialspace': False,576 'strict': False,577 'replace_nulls_with': None578 }579 580 @classmethod581 def extend(cls, dialect, fmtparams=None):582 if isinstance(dialect, str):583 dialect = get_dialect(dialect)584 585 if fmtparams is None:586 return dialect587 588 defaults = cls.defaults()589 590 if any(param not in defaults for param in fmtparams):591 raise TypeError('Invalid fmtparam')592 593 specified = dict(594 (attr, getattr(dialect, attr, None))595 for attr in cls.defaults()596 )597 598 specified.update(fmtparams)599 return type(str('ExtendedDialect'), (cls,), specified)600 601 @classmethod602 def combine(cls, dialect, fmtparams):603 """Create a new dialect with defaults and added parameters."""604 dialect = cls.extend(dialect, fmtparams)605 defaults = cls.defaults()606 specified = dict(607 (attr, getattr(dialect, attr, None))608 for attr in defaults609 if getattr(dialect, attr, None) is not None or610 attr in ['quotechar', 'delimiter', 'lineterminator', 'quoting']611 )612 613 defaults.update(specified)614 dialect = type(str('CombinedDialect'), (cls,), defaults)615 cls.validate(dialect)616 return dialect()617 618 def __delattr__(self, attr):619 if self._valid:620 raise AttributeError('dialect is immutable.')621 super().__delattr__(attr)622 623 def __setattr__(self, attr, value):624 if self._valid:625 raise AttributeError('dialect is immutable.')626 super().__setattr__(attr, value)627 628 629class Excel(Dialect):630 """Describe the usual properties of Excel-generated CSV files."""631 delimiter = ','632 quotechar = '"'633 doublequote = True634 skipinitialspace = False635 lineterminator = '\r\n'636 quoting = QUOTE_MINIMAL637 638 639register_dialect("excel", Excel)640 641 642class ExcelTab(Excel):643 """Describe the usual properties of Excel-generated TAB-delimited files."""644 delimiter = '\t'645 646 647register_dialect("excel-tab", ExcelTab)648 649 650class UnixDialect(Dialect):651 """Describe the usual properties of Unix-generated CSV files."""652 delimiter = ','653 quotechar = '"'654 doublequote = True655 skipinitialspace = False656 lineterminator = '\n'657 quoting = QUOTE_ALL658 659 660register_dialect("unix", UnixDialect)661 662 663class DictReader():664 def __init__(self, f, fieldnames=None, restkey=None, restval=None,665 *args, **kwds):666 self._fieldnames = fieldnames # list of keys for the dict667 self.restkey = restkey # key to catch long rows668 self.restval = restval # default value for short rows669 self.dialect = kwds.get('dialect', "excel")670 self.reader = Reader(f, self.dialect, *args, **kwds)671 self.line_num = 0672 673 def __iter__(self):674 return self675 676 @property677 def fieldnames(self):678 if self._fieldnames is None:679 try:680 self._fieldnames = next(self.reader)681 except StopIteration:682 pass683 self.line_num = self.reader.line_num684 return self._fieldnames685 686 @fieldnames.setter687 def fieldnames(self, value):688 self._fieldnames = value689 690 def __next__(self):691 if self.line_num == 0:692 # Used only for its side effect.693 self.fieldnames694 row = next(self.reader)695 self.line_num = self.reader.line_num696 697 # unlike the basic reader, we prefer not to return blanks,698 # because we will typically wind up with a dict full of None699 # values700 while row == []:701 row = next(self.reader)702 d = dict(zip(self.fieldnames, row))703 lf = len(self.fieldnames)704 lr = len(row)705 if lf < lr:706 d[self.restkey] = row[lf:]707 elif lf > lr:708 for key in self.fieldnames[lr:]:709 d[key] = self.restval710 return d711 712 next = __next__713 714 715class DictWriter():716 def __init__(self, f, fieldnames, *args, **kwds):717 self.fieldnames = fieldnames # list of keys for the dict718 self.extrasaction = kwds.get('extrasaction', "raise")719 self.restval = kwds.get('restval', "") # for writing short dicts720 if self.extrasaction.lower() not in ("raise", "ignore"):721 raise ValueError("extrasaction (%s) must be 'raise' or 'ignore'"722 % self.extrasaction)723 dialect = kwds.get('dialect', "excel")724 self.writer = Writer(f, dialect, *args, **kwds)725 726 def writeheader(self):727 header = dict(zip(self.fieldnames, self.fieldnames))728 self.writerow(header)729 730 def _dict_to_list(self, rowdict):731 if self.extrasaction == "raise":732 wrong_fields = [k for k in rowdict if k not in self.fieldnames]733 if wrong_fields:734 raise ValueError("dict contains fields not in fieldnames: " +735 ", ".join([repr(x) for x in wrong_fields]))736 return (rowdict.get(key, self.restval) for key in self.fieldnames)737 738 def writerow(self, rowdict):739 return self.writer.writerow(self._dict_to_list(rowdict))740 741 def writerows(self, rowdicts):742 return self.writer.writerows(map(self._dict_to_list, rowdicts))743 