Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/connectors/pyodbc.py: 23%
99 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1# connectors/pyodbc.py
2# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
8import re
10from . import Connector
11from .. import util
14class PyODBCConnector(Connector):
15 driver = "pyodbc"
17 # this is no longer False for pyodbc in general
18 supports_sane_rowcount_returning = True
19 supports_sane_multi_rowcount = False
21 supports_unicode_statements = True
22 supports_unicode_binds = True
24 supports_native_decimal = True
25 default_paramstyle = "named"
27 use_setinputsizes = False
29 # for non-DSN connections, this *may* be used to
30 # hold the desired driver name
31 pyodbc_driver_name = None
33 def __init__(
34 self, supports_unicode_binds=None, use_setinputsizes=False, **kw
35 ):
36 super(PyODBCConnector, self).__init__(**kw)
37 if supports_unicode_binds is not None:
38 self.supports_unicode_binds = supports_unicode_binds
39 self.use_setinputsizes = use_setinputsizes
41 @classmethod
42 def dbapi(cls):
43 return __import__("pyodbc")
45 def create_connect_args(self, url):
46 opts = url.translate_connect_args(username="user")
47 opts.update(url.query)
49 keys = opts
51 query = url.query
53 connect_args = {}
54 for param in ("ansi", "unicode_results", "autocommit"):
55 if param in keys:
56 connect_args[param] = util.asbool(keys.pop(param))
58 if "odbc_connect" in keys:
59 connectors = [util.unquote_plus(keys.pop("odbc_connect"))]
60 else:
62 def check_quote(token):
63 if ";" in str(token) or str(token).startswith("{"):
64 token = "{%s}" % token.replace("}", "}}")
65 return token
67 keys = dict((k, check_quote(v)) for k, v in keys.items())
69 dsn_connection = "dsn" in keys or (
70 "host" in keys and "database" not in keys
71 )
72 if dsn_connection:
73 connectors = [
74 "dsn=%s" % (keys.pop("host", "") or keys.pop("dsn", ""))
75 ]
76 else:
77 port = ""
78 if "port" in keys and "port" not in query:
79 port = ",%d" % int(keys.pop("port"))
81 connectors = []
82 driver = keys.pop("driver", self.pyodbc_driver_name)
83 if driver is None and keys:
84 # note if keys is empty, this is a totally blank URL
85 util.warn(
86 "No driver name specified; "
87 "this is expected by PyODBC when using "
88 "DSN-less connections"
89 )
90 else:
91 connectors.append("DRIVER={%s}" % driver)
93 connectors.extend(
94 [
95 "Server=%s%s" % (keys.pop("host", ""), port),
96 "Database=%s" % keys.pop("database", ""),
97 ]
98 )
100 user = keys.pop("user", None)
101 if user:
102 connectors.append("UID=%s" % user)
103 pwd = keys.pop("password", "")
104 if pwd:
105 connectors.append("PWD=%s" % pwd)
106 else:
107 authentication = keys.pop("authentication", None)
108 if authentication:
109 connectors.append("Authentication=%s" % authentication)
110 else:
111 connectors.append("Trusted_Connection=Yes")
113 # if set to 'Yes', the ODBC layer will try to automagically
114 # convert textual data from your database encoding to your
115 # client encoding. This should obviously be set to 'No' if
116 # you query a cp1253 encoded database from a latin1 client...
117 if "odbc_autotranslate" in keys:
118 connectors.append(
119 "AutoTranslate=%s" % keys.pop("odbc_autotranslate")
120 )
122 connectors.extend(["%s=%s" % (k, v) for k, v in keys.items()])
124 return [[";".join(connectors)], connect_args]
126 def is_disconnect(self, e, connection, cursor):
127 if isinstance(e, self.dbapi.ProgrammingError):
128 return "The cursor's connection has been closed." in str(
129 e
130 ) or "Attempt to use a closed connection." in str(e)
131 else:
132 return False
134 def _dbapi_version(self):
135 if not self.dbapi:
136 return ()
137 return self._parse_dbapi_version(self.dbapi.version)
139 def _parse_dbapi_version(self, vers):
140 m = re.match(r"(?:py.*-)?([\d\.]+)(?:-(\w+))?", vers)
141 if not m:
142 return ()
143 vers = tuple([int(x) for x in m.group(1).split(".")])
144 if m.group(2):
145 vers += (m.group(2),)
146 return vers
148 def _get_server_version_info(self, connection, allow_chars=True):
149 # NOTE: this function is not reliable, particularly when
150 # freetds is in use. Implement database-specific server version
151 # queries.
152 dbapi_con = connection.connection
153 version = []
154 r = re.compile(r"[.\-]")
155 for n in r.split(dbapi_con.getinfo(self.dbapi.SQL_DBMS_VER)):
156 try:
157 version.append(int(n))
158 except ValueError:
159 if allow_chars:
160 version.append(n)
161 return tuple(version)
163 def do_set_input_sizes(self, cursor, list_of_tuples, context):
164 # the rules for these types seems a little strange, as you can pass
165 # non-tuples as well as tuples, however it seems to assume "0"
166 # for the subsequent values if you don't pass a tuple which fails
167 # for types such as pyodbc.SQL_WLONGVARCHAR, which is the datatype
168 # that ticket #5649 is targeting.
170 # NOTE: as of #6058, this won't be called if the use_setinputsizes flag
171 # is False, or if no types were specified in list_of_tuples
173 cursor.setinputsizes(
174 [
175 (dbtype, None, None)
176 if not isinstance(dbtype, tuple)
177 else dbtype
178 for key, dbtype, sqltype in list_of_tuples
179 ]
180 )
182 def set_isolation_level(self, connection, level):
183 # adjust for ConnectionFairy being present
184 # allows attribute set e.g. "connection.autocommit = True"
185 # to work properly
186 if hasattr(connection, "dbapi_connection"):
187 connection = connection.dbapi_connection
189 if level == "AUTOCOMMIT":
190 connection.autocommit = True
191 else:
192 connection.autocommit = False
193 super(PyODBCConnector, self).set_isolation_level(connection, level)