1 """adodbapi.apibase - A python DB API 2.0 (PEP 249) interface to Microsoft ADO 2 3 Copyright (C) 2002 Henrik Ekelund, version 2.1 by Vernon Cole 4 * https://sourceforge.net/projects/pywin32 5 * https://sourceforge.net/projects/adodbapi 6 """ 7 8 from __future__ import annotations 9 10 import datetime 11 import decimal 12 import numbers 13 import sys 14 import time 15 from collections.abc import Callable, Iterable, Mapping 16 17 # noinspection PyUnresolvedReferences 18 from . import ado_consts as adc 19 20 verbose = False # debugging flag 21 22 23 # ------- Error handlers ------ 24 def standardErrorHandler(connection, cursor, errorclass, errorvalue): 25 err = (errorclass, errorvalue) 26 try: 27 connection.messages.append(err) 28 except: 29 pass 30 if cursor is not None: 31 try: 32 cursor.messages.append(err) 33 except: 34 pass 35 raise errorclass(errorvalue) 36 37 38 class Error(Exception): 39 pass # Exception that is the base class of all other error 40 # exceptions. You can use this to catch all errors with one 41 # single 'except' statement. Warnings are not considered 42 # errors and thus should not use this class as base. It must 43 # be a subclass of the Python StandardError (defined in the 44 # module exceptions). 45 46 47 class Warning(Exception): 48 pass 49 50 51 class InterfaceError(Error): 52 pass 53 54 55 class DatabaseError(Error): 56 pass 57 58 59 class InternalError(DatabaseError): 60 pass 61 62 63 class OperationalError(DatabaseError): 64 pass 65 66 67 class ProgrammingError(DatabaseError): 68 pass 69 70 71 class IntegrityError(DatabaseError): 72 pass 73 74 75 class DataError(DatabaseError): 76 pass 77 78 79 class NotSupportedError(DatabaseError): 80 pass 81 82 83 class FetchFailedError(OperationalError): 84 """ 85 Error is used by RawStoredProcedureQuerySet to determine when a fetch 86 failed due to a connection being closed or there is no record set 87 returned. (Non-standard, added especially for django) 88 """ 89 90 pass 91 92 93 # # # # # ----- Type Objects and Constructors ----- # # # # # 94 # Many databases need to have the input in a particular format for binding to an operation's input parameters. 95 # For example, if an input is destined for a DATE column, then it must be bound to the database in a particular 96 # string format. Similar problems exist for "Row ID" columns or large binary items (e.g. blobs or RAW columns). 97 # This presents problems for Python since the parameters to the executeXXX() method are untyped. 98 # When the database module sees a Python string object, it doesn't know if it should be bound as a simple CHAR 99 # column, as a raw BINARY item, or as a DATE. 100 # 101 # To overcome this problem, a module must provide the constructors defined below to create objects that can 102 # hold special values. When passed to the cursor methods, the module can then detect the proper type of 103 # the input parameter and bind it accordingly. 104 105 # A Cursor Object's description attribute returns information about each of the result columns of a query. 106 # The type_code must compare equal to one of Type Objects defined below. Type Objects may be equal to more than 107 # one type code (e.g. DATETIME could be equal to the type codes for date, time and timestamp columns; 108 # see the Implementation Hints below for details). 109 110 # SQL NULL values are represented by the Python None singleton on input and output. 111 112 # Note: Usage of Unix ticks for database interfacing can cause troubles because of the limited date range they cover. 113 114 115 # def Date(year,month,day): 116 # "This function constructs an object holding a date value. " 117 # return dateconverter.date(year,month,day) #dateconverter.Date(year,month,day) 118 # 119 # def Time(hour,minute,second): 120 # "This function constructs an object holding a time value. " 121 # return dateconverter.time(hour, minute, second) # dateconverter.Time(hour,minute,second) 122 # 123 # def Timestamp(year,month,day,hour,minute,second): 124 # "This function constructs an object holding a time stamp value. " 125 # return dateconverter.datetime(year,month,day,hour,minute,second) 126 # 127 # def DateFromTicks(ticks): 128 # """This function constructs an object holding a date value from the given ticks value 129 # (number of seconds since the epoch; see the documentation of the standard Python time module for details). """ 130 # return Date(*time.gmtime(ticks)[:3]) 131 # 132 # def TimeFromTicks(ticks): 133 # """This function constructs an object holding a time value from the given ticks value 134 # (number of seconds since the epoch; see the documentation of the standard Python time module for details). """ 135 # return Time(*time.gmtime(ticks)[3:6]) 136 # 137 # def TimestampFromTicks(ticks): 138 # """This function constructs an object holding a time stamp value from the given 139 # ticks value (number of seconds since the epoch; 140 # see the documentation of the standard Python time module for details). """ 141 # return Timestamp(*time.gmtime(ticks)[:6]) 142 # 143 # def Binary(aString): 144 # """This function constructs an object capable of holding a binary (long) string value. """ 145 # b = bytes(aString) 146 # return b 147 # ----- Time converters ---------------------------------------------- 148 class TimeConverter: # this is a generic time converter skeleton 149 def __init__(self): # the details will be filled in by instances 150 self._ordinal_1899_12_31 = datetime.date(1899, 12, 31).toordinal() - 1 151 # Use cls.types to compare if an input parameter is a datetime 152 self.types = { 153 # Dynamically get the types as the methods may be overriden 154 type(self.Date(2000, 1, 1)), 155 type(self.Time(12, 1, 1)), 156 type(self.Timestamp(2000, 1, 1, 12, 1, 1)), 157 datetime.datetime, 158 datetime.time, 159 datetime.date, 160 } 161 162 def COMDate(self, obj): 163 """Returns a ComDate from a date-time""" 164 try: # most likely a datetime 165 tt = obj.timetuple() 166 167 try: 168 ms = obj.microsecond 169 except: 170 ms = 0 171 return self.ComDateFromTuple(tt, ms) 172 except: # might be a tuple 173 try: 174 return self.ComDateFromTuple(obj) 175 except: 176 raise ValueError(f'Cannot convert "{obj!r}" to COMdate.') 177 178 def ComDateFromTuple(self, t, microseconds=0): 179 d = datetime.date(t[0], t[1], t[2]) 180 integerPart = d.toordinal() - self._ordinal_1899_12_31 181 ms = (t[3] * 3600 + t[4] * 60 + t[5]) * 1000000 + microseconds 182 fractPart = float(ms) / 86400000000.0 183 return integerPart + fractPart 184 185 def DateObjectFromCOMDate(self, comDate): 186 "Returns an object of the wanted type from a ComDate" 187 raise NotImplementedError # "Abstract class" 188 189 def Date(self, year, month, day): 190 "This function constructs an object holding a date value." 191 raise NotImplementedError # "Abstract class" 192 193 def Time(self, hour, minute, second): 194 "This function constructs an object holding a time value." 195 raise NotImplementedError # "Abstract class" 196 197 def Timestamp(self, year, month, day, hour, minute, second): 198 "This function constructs an object holding a time stamp value." 199 raise NotImplementedError # "Abstract class" 200 # all purpose date to ISO format converter 201 202 def DateObjectToIsoFormatString(self, obj): 203 "This function should return a string in the format 'YYYY-MM-dd HH:MM:SS:ms' (ms optional)" 204 try: # most likely, a datetime.datetime 205 s = obj.isoformat(" ") 206 except (TypeError, AttributeError): 207 if isinstance(obj, datetime.date): 208 s = obj.isoformat() + " 00:00:00" # return exact midnight 209 else: 210 try: # but may be time.struct_time 211 s = time.strftime("%Y-%m-%d %H:%M:%S", obj) 212 except: 213 raise ValueError(f'Cannot convert "{obj!r}" to isoformat') 214 return s 215 216 217 class pythonDateTimeConverter(TimeConverter): # standard since Python 2.3 218 def __init__(self): 219 TimeConverter.__init__(self) 220 221 def DateObjectFromCOMDate(self, comDate): 222 if isinstance(comDate, datetime.datetime): 223 odn = comDate.toordinal() 224 tim = comDate.time() 225 new = datetime.datetime.combine(datetime.datetime.fromordinal(odn), tim) 226 return new 227 # return comDate.replace(tzinfo=None) # make non aware 228 else: 229 fComDate = float(comDate) # ComDate is number of days since 1899-12-31 230 integerPart = int(fComDate) 231 floatpart = fComDate - integerPart 232 ##if floatpart == 0.0: 233 ## return datetime.date.fromordinal(integerPart + self._ordinal_1899_12_31) 234 dte = datetime.datetime.fromordinal( 235 integerPart + self._ordinal_1899_12_31 236 ) + datetime.timedelta(milliseconds=floatpart * 86400000) 237 # millisecondsperday=86400000 # 24*60*60*1000 238 return dte 239 240 def Date(self, year, month, day): 241 return datetime.date(year, month, day) 242 243 def Time(self, hour, minute, second): 244 return datetime.time(hour, minute, second) 245 246 def Timestamp(self, year, month, day, hour, minute, second): 247 return datetime.datetime(year, month, day, hour, minute, second) 248 249 250 class pythonTimeConverter(TimeConverter): # the old, ?nix type date and time 251 def __init__(self): # caution: this Class gets confised by timezones and DST 252 TimeConverter.__init__(self) 253 self.types.add(time.struct_time) 254 255 def DateObjectFromCOMDate(self, comDate): 256 "Returns ticks since 1970" 257 if isinstance(comDate, datetime.datetime): 258 return comDate.timetuple() 259 else: 260 fcomDate = float(comDate) 261 secondsperday = 86400 # 24*60*60 262 # ComDate is number of days since 1899-12-31, gmtime epoch is 1970-1-1 = 25569 days 263 t = time.gmtime(secondsperday * (fcomDate - 25569.0)) 264 return t # year,month,day,hour,minute,second,weekday,julianday,daylightsaving=t 265 266 def Date(self, year, month, day): 267 return self.Timestamp(year, month, day, 0, 0, 0) 268 269 def Time(self, hour, minute, second): 270 return time.gmtime((hour * 60 + minute) * 60 + second) 271 272 def Timestamp(self, year, month, day, hour, minute, second): 273 return time.localtime( 274 time.mktime((year, month, day, hour, minute, second, 0, 0, -1)) 275 ) 276 277 278 base_dateconverter = pythonDateTimeConverter() 279 280 # ------ DB API required module attributes --------------------- 281 threadsafety = 1 # TODO -- find out whether this module is actually BETTER than 1. 282 283 apilevel = "2.0" # String constant stating the supported DB API level. 284 285 paramstyle = "qmark" # the default parameter style 286 287 # ------ control for an extension which may become part of DB API 3.0 --- 288 accepted_paramstyles = ("qmark", "named", "format", "pyformat", "dynamic") 289 290 # ------------------------------------------------------------------------------------------ 291 # define similar types for generic conversion routines 292 adoIntegerTypes = ( 293 adc.adInteger, 294 adc.adSmallInt, 295 adc.adTinyInt, 296 adc.adUnsignedInt, 297 adc.adUnsignedSmallInt, 298 adc.adUnsignedTinyInt, 299 adc.adBoolean, 300 adc.adError, 301 ) # max 32 bits 302 adoRowIdTypes = (adc.adChapter,) # v2.1 Rose 303 adoLongTypes = (adc.adBigInt, adc.adFileTime, adc.adUnsignedBigInt) 304 adoExactNumericTypes = ( 305 adc.adDecimal, 306 adc.adNumeric, 307 adc.adVarNumeric, 308 adc.adCurrency, 309 ) # v2.3 Cole 310 adoApproximateNumericTypes = (adc.adDouble, adc.adSingle) # v2.1 Cole 311 adoStringTypes = ( 312 adc.adBSTR, 313 adc.adChar, 314 adc.adLongVarChar, 315 adc.adLongVarWChar, 316 adc.adVarChar, 317 adc.adVarWChar, 318 adc.adWChar, 319 ) 320 adoBinaryTypes = (adc.adBinary, adc.adLongVarBinary, adc.adVarBinary) 321 adoDateTimeTypes = (adc.adDBTime, adc.adDBTimeStamp, adc.adDate, adc.adDBDate) 322 adoRemainingTypes = ( 323 adc.adEmpty, 324 adc.adIDispatch, 325 adc.adIUnknown, 326 adc.adPropVariant, 327 adc.adArray, 328 adc.adUserDefined, 329 adc.adVariant, 330 adc.adGUID, 331 ) 332 333 334 # this class is a trick to determine whether a type is a member of a related group of types. see PEP notes 335 class DBAPITypeObject: 336 def __init__(self, valuesTuple): 337 self.values = frozenset(valuesTuple) 338 339 def __eq__(self, other): 340 return other in self.values 341 342 def __ne__(self, other): 343 return other not in self.values 344 345 346 """This type object is used to describe columns in a database that are string-based (e.g. CHAR). """ 347 STRING = DBAPITypeObject(adoStringTypes) 348 349 """This type object is used to describe (long) binary columns in a database (e.g. LONG, RAW, BLOBs). """ 350 BINARY = DBAPITypeObject(adoBinaryTypes) 351 352 """This type object is used to describe numeric columns in a database. """ 353 NUMBER = DBAPITypeObject( 354 adoIntegerTypes + adoLongTypes + adoExactNumericTypes + adoApproximateNumericTypes 355 ) 356 357 """This type object is used to describe date/time columns in a database. """ 358 359 DATETIME = DBAPITypeObject(adoDateTimeTypes) 360 """This type object is used to describe the "Row ID" column in a database. """ 361 ROWID = DBAPITypeObject(adoRowIdTypes) 362 363 OTHER = DBAPITypeObject(adoRemainingTypes) 364 365 # ------- utilities for translating python data types to ADO data types --------------------------------- 366 typeMap = { 367 memoryview: adc.adVarBinary, 368 float: adc.adDouble, 369 type(None): adc.adEmpty, 370 str: adc.adBSTR, 371 bool: adc.adBoolean, # v2.1 Cole 372 decimal.Decimal: adc.adDecimal, 373 int: adc.adBigInt, 374 bytes: adc.adVarBinary, 375 } 376 377 378 def pyTypeToADOType(d): 379 tp = type(d) 380 try: 381 return typeMap[tp] 382 except KeyError: # The type was not defined in the pre-computed Type table 383 from . import dateconverter 384 385 # maybe it is one of our supported Date/Time types 386 if tp in dateconverter.types: 387 return adc.adDate 388 # otherwise, attempt to discern the type by probing the data object itself -- to handle duck typing 389 if isinstance(d, str): 390 return adc.adBSTR 391 if isinstance(d, numbers.Integral): 392 return adc.adBigInt 393 if isinstance(d, numbers.Real): 394 return adc.adDouble 395 raise DataError(f'cannot convert "{d!r}" (type={tp}) to ADO') 396 397 398 # # # # # # # # # # # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 399 # functions to convert database values to Python objects 400 # ------------------------------------------------------------------------ 401 # variant type : function converting variant to Python value 402 def variantConvertDate(v): 403 from . import dateconverter # this function only called when adodbapi is running 404 405 return dateconverter.DateObjectFromCOMDate(v) 406 407 408 def cvtString(variant): # use to get old action of adodbapi v1 if desired 409 return str(variant) 410 411 412 def cvtDecimal(variant): # better name 413 return _convertNumberWithCulture(variant, decimal.Decimal) 414 415 416 def cvtNumeric(variant): # older name - don't break old code 417 return cvtDecimal(variant) 418 419 420 def cvtFloat(variant): 421 return _convertNumberWithCulture(variant, float) 422 423 424 def _convertNumberWithCulture(variant, f): 425 try: 426 return f(variant) 427 except (ValueError, TypeError, decimal.InvalidOperation): 428 try: 429 europeVsUS = str(variant).replace(",", ".") 430 return f(europeVsUS) 431 except (ValueError, TypeError, decimal.InvalidOperation): 432 pass 433 434 435 def cvtInt(variant): 436 return int(variant) 437 438 439 def cvtLong(variant): # only important in old versions where long and int differ 440 return int(variant) 441 442 443 def cvtBuffer(variant): 444 return bytes(variant) 445 446 447 def cvtUnicode(variant): 448 return str(variant) 449 450 451 def identity(x): 452 return x 453 454 455 def cvtUnusual(variant): 456 if verbose > 1: 457 sys.stderr.write(f"Conversion called for Unusual data={variant!r}\n") 458 return variant # cannot find conversion function -- just give the data to the user 459 460 461 def convert_to_python(variant, func): # convert DB value into Python value 462 if variant is None: 463 return None 464 return func(variant) # call the appropriate conversion function 465 466 467 class MultiMap(dict[int, Callable[[object], object]]): 468 # builds a dictionary from {(iterable,of,keys) : function} 469 """A dictionary of ado.type : function 470 -- but you can set multiple items by passing an iterable of keys""" 471 472 # useful for defining conversion functions for groups of similar data types. 473 def __init__(self, aDict: Mapping[Iterable[int] | int, Callable[[object], object]]): 474 for k, v in aDict.items(): 475 self[k] = v # we must call __setitem__ 476 477 def __setitem__( 478 self, adoType: Iterable[int] | int, cvtFn: Callable[[object], object] 479 ): 480 "set a single item, or a whole iterable of items" 481 if isinstance(adoType, Iterable): 482 # user passed us an iterable, set them individually 483 for type in adoType: 484 dict.__setitem__(self, type, cvtFn) 485 else: 486 dict.__setitem__(self, adoType, cvtFn) 487 488 489 # initialize variantConversions dictionary used to convert SQL to Python 490 # this is the dictionary of default conversion functions, built by the class above. 491 # this becomes a class attribute for the Connection, and that attribute is used 492 # to build the list of column conversion functions for the Cursor 493 variantConversions = MultiMap( 494 { 495 adoDateTimeTypes: variantConvertDate, 496 adoApproximateNumericTypes: cvtFloat, 497 adoExactNumericTypes: cvtDecimal, # use to force decimal rather than unicode 498 adoLongTypes: cvtLong, 499 adoIntegerTypes: cvtInt, 500 adoRowIdTypes: cvtInt, 501 adoStringTypes: identity, 502 adoBinaryTypes: cvtBuffer, 503 adoRemainingTypes: cvtUnusual, 504 } 505 ) 506 507 # # # # # classes to emulate the result of cursor.fetchxxx() as a sequence of sequences # # # # # 508 # "an ENUM of how my low level records are laid out" 509 RS_WIN_32, RS_ARRAY, RS_REMOTE = list(range(1, 4)) 510 511 512 class SQLrow: # a single database row 513 # class to emulate a sequence, so that a column may be retrieved by either number or name 514 def __init__(self, rows, index): # "rows" is an _SQLrows object, index is which row 515 self.rows = rows # parent 'fetch' container object 516 self.index = index # my row number within parent 517 518 def __getattr__(self, name): # used for row.columnName type of value access 519 try: 520 return self._getValue(self.rows.columnNames[name.lower()]) 521 except KeyError: 522 raise AttributeError('Unknown column name "{}"'.format(name)) 523 524 def _getValue(self, key): # key must be an integer 525 if ( 526 self.rows.recordset_format == RS_ARRAY 527 ): # retrieve from two-dimensional array 528 v = self.rows.ado_results[key, self.index] 529 elif self.rows.recordset_format == RS_REMOTE: 530 v = self.rows.ado_results[self.index][key] 531 else: # pywin32 - retrieve from tuple of tuples 532 v = self.rows.ado_results[key][self.index] 533 if self.rows.converters is NotImplemented: 534 return v 535 return convert_to_python(v, self.rows.converters[key]) 536 537 def __len__(self): 538 return self.rows.numberOfColumns 539 540 def __getitem__(self, key): # used for row[key] type of value access 541 if isinstance(key, int): # normal row[1] designation 542 try: 543 return self._getValue(key) 544 except IndexError: 545 raise 546 if isinstance(key, slice): 547 indices = key.indices(self.rows.numberOfColumns) 548 vl = [self._getValue(i) for i in range(*indices)] 549 return tuple(vl) 550 try: 551 return self._getValue( 552 self.rows.columnNames[key.lower()] 553 ) # extension row[columnName] designation 554 except (KeyError, TypeError): 555 er, st, tr = sys.exc_info() 556 raise er(f'No such key as "{key!r}" in {self!r}').with_traceback(tr) 557 558 def __iter__(self): 559 return iter(self.__next__()) 560 561 def __next__(self): 562 for n in range(self.rows.numberOfColumns): 563 yield self._getValue(n) 564 565 def __repr__(self): # create a human readable representation 566 taglist = sorted(list(self.rows.columnNames.items()), key=lambda x: x[1]) 567 s = "<SQLrow={" 568 for name, i in taglist: 569 s += f"{name}:{self._getValue(i)!r}, " 570 return s[:-2] + "}>" 571 572 def __str__(self): # create a pretty human readable representation 573 return str( 574 tuple(str(self._getValue(i)) for i in range(self.rows.numberOfColumns)) 575 ) 576 577 # TO-DO implement pickling an SQLrow directly 578 # def __getstate__(self): return self.__dict__ 579 # def __setstate__(self, d): self.__dict__.update(d) 580 # which basically tell pickle to treat your class just like a normal one, 581 # taking self.__dict__ as representing the whole of the instance state, 582 # despite the existence of the __getattr__. 583 # # # # 584 585 586 class SQLrows: 587 # class to emulate a sequence for multiple rows using a container object 588 def __init__(self, ado_results, numberOfRows, cursor): 589 self.ado_results = ado_results # raw result of SQL get 590 try: 591 self.recordset_format = cursor.recordset_format 592 self.numberOfColumns = cursor.numberOfColumns 593 self.converters = cursor.converters 594 self.columnNames = cursor.columnNames 595 except AttributeError: 596 self.recordset_format = RS_ARRAY 597 self.numberOfColumns = 0 598 self.converters = [] 599 self.columnNames = {} 600 self.numberOfRows = numberOfRows 601 602 def __len__(self): 603 return self.numberOfRows 604 605 def __getitem__(self, item): # used for row or row,column access 606 if not self.ado_results: 607 return [] 608 if isinstance(item, slice): # will return a list of row objects 609 indices = item.indices(self.numberOfRows) 610 return [SQLrow(self, k) for k in range(*indices)] 611 elif isinstance(item, tuple) and len(item) == 2: 612 # d = some_rowsObject[i,j] will return a datum from a two-dimension address 613 i, j = item 614 if not isinstance(j, int): 615 try: 616 j = self.columnNames[j.lower()] # convert named column to numeric 617 except KeyError: 618 raise KeyError(f"adodbapi: no such column name as {j!r}") 619 if self.recordset_format == RS_ARRAY: # retrieve from two-dimensional array 620 v = self.ado_results[j, i] 621 elif self.recordset_format == RS_REMOTE: 622 v = self.ado_results[i][j] 623 else: # pywin32 - retrieve from tuple of tuples 624 v = self.ado_results[j][i] 625 if self.converters is NotImplemented: 626 return v 627 return convert_to_python(v, self.converters[j]) 628 else: 629 row = SQLrow(self, item) # new row descriptor 630 return row 631 632 def __iter__(self): 633 return iter(self.__next__()) 634 635 def __next__(self): 636 for n in range(self.numberOfRows): 637 row = SQLrow(self, n) 638 yield row 639 # # # # # 640 641 # # # # # functions to re-format SQL requests to other paramstyle requirements # # # # # # # # # # 642 643 644 def changeNamedToQmark( 645 op, 646 ): # convert from 'named' paramstyle to ADO required '?'mark parameters 647 outOp = "" 648 outparms = [] 649 chunks = op.split( 650 "'" 651 ) # quote all literals -- odd numbered list results are literals. 652 inQuotes = False 653 for chunk in chunks: 654 if inQuotes: # this is inside a quote 655 if chunk == "": # double apostrophe to quote one apostrophe 656 outOp = outOp[:-1] # so take one away 657 else: 658 outOp += "'" + chunk + "'" # else pass the quoted string as is. 659 else: # is SQL code -- look for a :namedParameter 660 while chunk: # some SQL string remains 661 sp = chunk.split(":", 1) 662 outOp += sp[0] # concat the part up to the : 663 s = "" 664 try: 665 chunk = sp[1] 666 except IndexError: 667 chunk = None 668 if chunk: # there was a parameter - parse it out 669 i = 0 670 c = chunk[0] 671 while c.isalnum() or c == "_": 672 i += 1 673 try: 674 c = chunk[i] 675 except IndexError: 676 break 677 s = chunk[:i] 678 chunk = chunk[i:] 679 if s: 680 outparms.append(s) # list the parameters in order 681 outOp += "?" # put in the Qmark 682 inQuotes = not inQuotes 683 return outOp, outparms 684 685 686 def changeFormatToQmark( 687 op, 688 ): # convert from 'format' paramstyle to ADO required '?'mark parameters 689 outOp = "" 690 outparams = [] 691 chunks = op.split( 692 "'" 693 ) # quote all literals -- odd numbered list results are literals. 694 inQuotes = False 695 for chunk in chunks: 696 if inQuotes: 697 if ( 698 outOp != "" and chunk == "" 699 ): # he used a double apostrophe to quote one apostrophe 700 outOp = outOp[:-1] # so take one away 701 else: 702 outOp += "'" + chunk + "'" # else pass the quoted string as is. 703 else: # is SQL code -- look for a %s parameter 704 if "%(" in chunk: # ugh! pyformat! 705 while chunk: # some SQL string remains 706 sp = chunk.split("%(", 1) 707 outOp += sp[0] # concat the part up to the % 708 if len(sp) > 1: 709 try: 710 s, chunk = sp[1].split(")s", 1) # find the ')s' 711 except ValueError: 712 raise ProgrammingError( 713 'Pyformat SQL has incorrect format near "%s"' % chunk 714 ) 715 outparams.append(s) 716 outOp += "?" # put in the Qmark 717 else: 718 chunk = None 719 else: # proper '%s' format 720 sp = chunk.split("%s") # make each %s 721 outOp += "?".join(sp) # into ? 722 inQuotes = not inQuotes # every other chunk is a quoted string 723 return outOp, outparams 724