Skip to content

Commit 5915f6a

Browse files
committed
Full adodbapi type-check and test fixes
1 parent 572e657 commit 5915f6a

17 files changed

Lines changed: 322 additions & 245 deletions

.git-blame-ignore-revs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
# 2024-10-14 formatted Python source with Ruff format
1818
2b5191d8fc6f1d1fbde01481b49278c1957ef8f1
19+
b74bfdca97238735adbd1b20d7245cca7070900f

.github/workflows/main.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,14 @@ jobs:
7171
# Pass Silent flag to regsvr32 to avoid hanging on confirmation window
7272
run: com/TestSources/PyCOMTest/buildAndRegister.bat /s
7373

74-
- name: Run tests
74+
- name: Run pywin32 tests
7575
# Run the tests directly from the source dir so support files (eg, .wav files etc)
7676
# can be found - they aren't installed into the Python tree.
7777
run: python win32/scripts/pywin32_testall.py -v -skip-adodbapi
7878

79+
- name: Run adodbapi tests
80+
run: python adodbapi/test/adodbapitest.py --all
81+
7982
- name: Build wheels
8083
run: pip wheel . -v --wheel-dir=dist
8184

@@ -196,6 +199,13 @@ jobs:
196199
- uses: jakebailey/pyright-action@v2
197200
with:
198201
python-version: ${{ matrix.python-version }}
199-
version: "1.1.401"
202+
version: "1.1.407"
200203
annotate: errors
201204
if: ${{ !cancelled() }} # Show issues even if the previous steps failed. But still fail the job
205+
206+
- uses: jakebailey/pyright-action@v2
207+
with:
208+
python-version: ${{ matrix.python-version }}
209+
version: "1.1.407"
210+
project: "pyrightconfig.adodbapi.json"
211+
if: ${{ !cancelled() }} # Show issues even if the previous steps failed. But still fail the job

adodbapi/adodbapi.py

Lines changed: 87 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,25 @@
2626
This module source should run correctly in CPython versions 2.7 and later,
2727
or CPython 3.4 or later.
2828
"""
29-
30-
__version__ = "2.6.2.0"
31-
version = "adodbapi v" + __version__
29+
from __future__ import annotations
3230

3331
import copy
3432
import decimal
3533
import os
3634
import sys
3735
import weakref
36+
from collections.abc import Iterable, Mapping, Sequence
37+
from typing import TYPE_CHECKING, Callable, Literal, NoReturn, cast
3838

3939
from . import ado_consts as adc, apibase as api, process_connect_string
4040

41+
if TYPE_CHECKING:
42+
from win32com.client import DispatchBaseClass
43+
from win32com.client.dynamic import CDispatch
44+
45+
__version__ = "2.6.2.0"
46+
version = "adodbapi v" + __version__
47+
4148
try:
4249
verbose = int(os.environ["ADODBAPI_VERBOSE"])
4350
except:
@@ -59,9 +66,6 @@ def getIndexedValue(obj, index):
5966
return obj(index)
6067

6168

62-
from collections.abc import Mapping
63-
64-
6569
# ----------------- The .connect method -----------------
6670
def make_COM_connecter():
6771
try:
@@ -175,9 +179,10 @@ def _configure_parameter(p, value, adotype, settings_known):
175179

176180
elif isinstance(value, decimal.Decimal):
177181
p.Value = value
178-
exponent = value.as_tuple()[2]
179-
digit_count = len(value.as_tuple()[1])
182+
exponent = value.as_tuple().exponent
183+
digit_count = len(value.as_tuple().digits)
180184
p.Precision = digit_count
185+
assert isinstance(exponent, int)
181186
if exponent == 0:
182187
p.NumericScale = 0
183188
elif exponent < 0:
@@ -224,25 +229,37 @@ class Connection:
224229
FetchFailedError = api.FetchFailedError # (special for django)
225230
# ...class attributes... (can be overridden by instance attributes)
226231
verbose = api.verbose
232+
# Can be set to override api.variantConversions in Cursor
233+
variantConversions: api.MultiMap
227234

228235
@property
229236
def dbapi(self): # a proposed db-api version 3 extension.
230237
"Return a reference to the DBAPI module for this Connection."
231238
return api
232239

233-
def __init__(self): # now define the instance attributes
234-
self.connector = None
240+
def __init__(self) -> None: # now define the instance attributes
241+
# Let it error if a connection is used before calling connect or after calling close
242+
self.connector: CDispatch | DispatchBaseClass = None # type: ignore[assignment]
235243
self.paramstyle = api.paramstyle
236244
self.supportsTransactions = False
237245
self.connection_string = ""
238246
self.cursors = weakref.WeakValueDictionary[int, Cursor]()
239247
self.dbms_name = ""
240248
self.dbms_version = ""
241-
self.errorhandler = None # use the standard error handler for this instance
249+
self.errorhandler: Callable[..., NoReturn] | None = (
250+
None # use the standard error handler for this instance
251+
)
242252
self.transaction_level = 0 # 0 == Not in a transaction, at the top level
243253
self._autocommit = False
244-
245-
def connect(self, kwargs, connection_maker=make_COM_connecter):
254+
self.messages: list[tuple[type[api.Error], api.Error]] = []
255+
256+
def connect(
257+
self,
258+
kwargs,
259+
connection_maker: Callable[
260+
[], CDispatch | DispatchBaseClass
261+
] = make_COM_connecter,
262+
):
246263
if verbose > 9:
247264
print(f"kwargs={kwargs!r}")
248265
try:
@@ -299,7 +316,7 @@ def connect(self, kwargs, connection_maker=make_COM_connecter):
299316
if verbose:
300317
print("adodbapi New connection at %X" % id(self))
301318

302-
def _raiseConnectionError(self, errorclass, errorvalue):
319+
def _raiseConnectionError(self, errorclass, errorvalue) -> NoReturn:
303320
eh = self.errorhandler
304321
if eh is None:
305322
eh = api.standardErrorHandler
@@ -337,7 +354,7 @@ def close(self):
337354
except Exception as e:
338355
self._raiseConnectionError(sys.exc_info()[0], sys.exc_info()[1])
339356

340-
self.connector = None # v2.4.2.2 fix subtle timeout bug
357+
self.connector = None # pyright: ignore[reportAttributeAccessIssue] # v2.4.2.2 fix subtle timeout bug
341358
# per M.Hammond: "I expect the benefits of uninitializing are probably fairly small,
342359
# so never uninitializing will probably not cause any problems."
343360

@@ -421,22 +438,20 @@ def __setattr__(self, name, value):
421438
value = copy.copy(value)
422439
object.__setattr__(self, name, value)
423440

424-
def __getattr__(self, item):
425-
if (
426-
item == "rollback"
427-
): # the rollback method only appears if the database supports transactions
428-
if self.supportsTransactions:
429-
return (
430-
self._rollback
431-
) # return the rollback method so the caller can execute it.
432-
else:
433-
raise AttributeError("this data provider does not support Rollback")
434-
elif item == "autocommit":
435-
return self._autocommit
441+
@property
442+
def rollback(self):
443+
"""Gets the rollback method so the caller can execute it.
444+
445+
The rollback method only appears if the database supports transactions
446+
"""
447+
if self.supportsTransactions:
448+
return self._rollback
436449
else:
437-
raise AttributeError(
438-
'no such attribute in ADO connection object as="%s"' % item
439-
)
450+
raise AttributeError("this data provider does not support Rollback")
451+
452+
@property
453+
def autocommit(self):
454+
return self._autocommit
440455

441456
def cursor(self):
442457
"Return a new Cursor Object using the connection."
@@ -465,7 +480,7 @@ def printADOerrors(self):
465480
print("Error: %s %s " % (e.Number, adc.adoErrors.get(e.Number, "unknown")))
466481
if e.Number == adc.ado_error_TIMEOUT:
467482
print(
468-
"Timeout Error: Try using adodbpi.connect(constr,timeout=Nseconds)"
483+
"Timeout Error: Try using adodbapi.connect(constr,timeout=Nseconds)"
469484
)
470485
print("Source: %s" % e.Source)
471486
print("NativeError: %s" % e.NativeError)
@@ -492,7 +507,7 @@ def __del__(self):
492507
self._closeAdoConnection() # v2.1 Rose
493508
except:
494509
pass
495-
self.connector = None
510+
self.connector = None # pyright: ignore[reportAttributeAccessIssue]
496511

497512
def __enter__(self): # Connections are context managers
498513
return self
@@ -542,17 +557,21 @@ class Cursor:
542557
## errorhandler...
543558
## allows the programmer to override the connection's default error handler
544559

545-
def __init__(self, connection):
560+
def __init__(self, connection: Connection) -> None:
546561
self.command = None
547-
self._ado_prepared = False
548-
self.messages = []
549-
self.connection = connection
550-
self.paramstyle = connection.paramstyle # used for overriding the paramstyle
551-
self._parameter_names = []
562+
self._ado_prepared: bool | Literal["setup"] = False
563+
self.messages: list[tuple[type[api.Error], api.Error]] = []
564+
self.connection: Connection = connection
565+
self.paramstyle = connection.paramstyle
566+
"""Used for overriding the paramstyle"""
567+
self._parameter_names: list[str] = []
552568
self.recordset_is_remote = False
553-
self.rs = None # the ADO recordset for this cursor
554-
self.converters = [] # conversion function for each column
555-
self.columnNames = {} # names of columns {lowercase name : number,...}
569+
self.rs = None
570+
"""The ADO recordset for this cursor"""
571+
self.converters: list[Callable[[object], object]] = []
572+
"""Conversion function for each column"""
573+
self.columnNames: dict[str, int] = {}
574+
"""Names of columns {lowercase name : number,...}"""
556575
self.numberOfColumns = 0
557576
self._description = None
558577
self.rowcount = -1
@@ -587,7 +606,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
587606
"Allow database cursors to be used with context managers."
588607
self.close()
589608

590-
def _raiseCursorError(self, errorclass, errorvalue):
609+
def _raiseCursorError(self, errorclass, errorvalue) -> NoReturn:
591610
eh = self.errorhandler
592611
if eh is None:
593612
eh = api.standardErrorHandler
@@ -613,9 +632,8 @@ def build_column_info(self, recordset):
613632
for i in range(self.numberOfColumns):
614633
f = getIndexedValue(self.rs.Fields, i)
615634
try:
616-
self.converters.append(
617-
varCon[f.Type]
618-
) # conversion function for this column
635+
# conversion function for this column
636+
self.converters.append(varCon[f.Type])
619637
except KeyError:
620638
self._raiseCursorError(
621639
api.InternalError, "Data column of Unknown ADO type=%s" % f.Type
@@ -633,7 +651,7 @@ def _makeDescriptionFromRS(self):
633651
if self.rs.EOF or self.rs.BOF:
634652
display_size = None
635653
else:
636-
# TODO: Is this the correct defintion according to the DB API 2 Spec ?
654+
# TODO: Is this the correct definition according to the DB API 2 Spec ?
637655
display_size = f.ActualSize
638656
null_ok = bool(f.Attributes & adc.adFldMayBeNull) # v2.1 Cole
639657
desc.append(
@@ -654,17 +672,16 @@ def get_description(self):
654672
self._makeDescriptionFromRS()
655673
return self._description
656674

657-
def __getattr__(self, item):
658-
if item == "description":
659-
return self.get_description()
660-
object.__getattribute__(
661-
self, item
662-
) # may get here on Remote attribute calls for existing attributes
675+
@property
676+
def description(self):
677+
return self.get_description()
663678

664679
def format_description(self, d):
665680
"""Format db_api description tuple for printing."""
666681
if self.description is None:
667682
self._makeDescriptionFromRS()
683+
if self.description is None:
684+
return "None"
668685
if isinstance(d, int):
669686
d = self.description[d]
670687
desc = (
@@ -690,17 +707,15 @@ def close(self, dont_tell_me=False):
690707
return
691708
self.messages = []
692709
if (
710+
# rs exists and is open #v2.1 Rose
693711
self.rs and self.rs.State != adc.adStateClosed
694-
): # rs exists and is open #v2.1 Rose
712+
):
695713
self.rs.Close() # v2.1 Rose
696714
self.rs = None # let go of the recordset so ADO will let it be disposed #v2.1 Rose
697715
if not dont_tell_me:
698-
self.connection._i_am_closing(
699-
self
700-
) # take me off the connection's cursors list
701-
self.connection = (
702-
None # this will make all future method calls on me throw an exception
703-
)
716+
# take me off the connection's cursors list
717+
self.connection._i_am_closing(self)
718+
self.connection = None # pyright: ignore[reportAttributeAccessIssue] # this will make all future method calls on me throw an exception
704719
if verbose:
705720
print("adodbapi Closed cursor at %X" % id(self))
706721

@@ -711,7 +726,6 @@ def __del__(self):
711726
pass
712727

713728
def _new_command(self, command_type=adc.adCmdText):
714-
self.cmd = None
715729
self.messages = []
716730

717731
if self.connection is None:
@@ -830,15 +844,16 @@ def _reformat_operation(self, operation, parameters):
830844
elif self.paramstyle == "named" or (
831845
self.paramstyle == "dynamic" and isinstance(parameters, Mapping)
832846
):
833-
operation, self._parameter_names = api.changeNamedToQmark(
834-
operation
835-
) # convert :name to ?
847+
# convert :name to ?
848+
operation, self._parameter_names = api.changeNamedToQmark(operation)
836849
return operation
837850

838-
def _buildADOparameterList(self, parameters, sproc=False):
851+
def _buildADOparameterList(
852+
self, parameters: Mapping[str, object] | Sequence[object] | None, sproc=False
853+
):
839854
self.parameters = parameters
840855
if parameters is None:
841-
parameters = []
856+
parameters = {}
842857

843858
# Note: ADO does not preserve the parameter list, even if "Prepared" is True, so we must build every time.
844859
parameters_known = False
@@ -866,6 +881,7 @@ def _buildADOparameterList(self, parameters, sproc=False):
866881
i = 0
867882
if parameters_known: # use ado parameter list
868883
if self._parameter_names: # named parameters
884+
assert isinstance(parameters, Mapping)
869885
for i, pm_name in enumerate(self._parameter_names):
870886
p = getIndexedValue(self.cmd.Parameters, i)
871887
try:
@@ -906,6 +922,7 @@ def _buildADOparameterList(self, parameters, sproc=False):
906922
else: # -- build own parameter list
907923
# we expect a dictionary of parameters, this is the list of expected names
908924
if self._parameter_names:
925+
assert isinstance(parameters, Mapping)
909926
for parm_name in self._parameter_names:
910927
elem = parameters[parm_name]
911928
adotype = api.pyTypeToADOType(elem)
@@ -957,9 +974,8 @@ def _buildADOparameterList(self, parameters, sproc=False):
957974
)
958975
i += 1
959976
if self._ado_prepared == "setup":
960-
self._ado_prepared = (
961-
True # parameters will be "known" by ADO next loop
962-
)
977+
# parameters will be "known" by ADO next loop
978+
self._ado_prepared = True
963979

964980
def execute(self, operation, parameters=None):
965981
"""Prepare and execute a database operation (query or command).
@@ -1013,9 +1029,9 @@ def executemany(self, operation, seq_of_parameters):
10131029
"""Prepare a database operation (query or command)
10141030
and then execute it against all parameter sequences or mappings found in the sequence seq_of_parameters.
10151031
1016-
Return values are not defined.
1032+
Return values are not defined.
10171033
"""
1018-
self.messages = list()
1034+
self.messages = []
10191035
total_recordcount = 0
10201036

10211037
self.prepare(operation)
@@ -1036,7 +1052,7 @@ def _fetch(self, limit=None):
10361052
self._raiseCursorError(
10371053
api.FetchFailedError, "fetch() on closed connection or empty query set"
10381054
)
1039-
return
1055+
return # Still return in case a custom errorHandler doesn't raise
10401056

10411057
if self.rs.State == adc.adStateClosed or self.rs.BOF or self.rs.EOF:
10421058
return list()

0 commit comments

Comments
 (0)