Skip to content

Commit 4c4603c

Browse files
Merge branch 'drnushooz-sqlalchemy-20'
2 parents cc4633c + 25ef08c commit 4c4603c

14 files changed

Lines changed: 94 additions & 104 deletions

NEWS.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,10 @@ Deleted Plugin import left behind in 0.2.2
170170

171171
* Fixed .rst file location in MANIFEST.in
172172
* Parse SQL comments in first line
173-
* Bugfixes for DSN, `--close`, others
173+
* Bugfixes for DSN, `--close`, others
174+
175+
0.5.0
176+
~~~~~
177+
178+
* Use SQLAlchemy 2.0
179+
* Drop undocumented support for dict-style access to raw row instances

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ Credits
426426
- vkk800 for ``--file``
427427
- Jens Albrecht for MySQL DatabaseError bugfix
428428
- meihkv for connection-closing bugfix
429+
- Abhinav C for SQLAlchemy 2.0 compatibility
429430

430431
.. _Distribute: http://pypi.python.org/pypi/distribute
431432
.. _Buildout: http://www.buildout.org/

requirements-dev.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
psycopg2
1+
psycopg2-binary
22
pandas
33
pytest
44
wheel
55
twine
66
readme-renderer
77
black
88
isort
9-

requirements.txt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
prettytable==0.7.2
2-
ipython>=1.0
3-
sqlalchemy>=0.6.7,<2.0
1+
prettytable
2+
ipython
3+
sqlalchemy>=2.0
44
sqlparse
55
six
6-
ipython-genutils>=0.1.0
6+
ipython-genutils
7+
traitlets
8+
matplotlib

setup.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
NEWS = open(os.path.join(here, "NEWS.rst"), encoding="utf-8").read()
99

1010

11-
version = "0.4.1"
11+
version = "0.5.0"
1212

1313
install_requires = [
14-
"prettytable<1",
15-
"ipython>=1.0",
16-
"sqlalchemy>=0.6.7",
14+
"prettytable",
15+
"ipython",
16+
"sqlalchemy>=2.0",
1717
"sqlparse",
1818
"six",
19-
"ipython-genutils>=0.1.0",
19+
"ipython-genutils",
2020
]
2121

2222

@@ -33,7 +33,6 @@
3333
"Topic :: Database",
3434
"Topic :: Database :: Front-Ends",
3535
"Programming Language :: Python :: 3",
36-
"Programming Language :: Python :: 2",
3736
],
3837
keywords="database ipython postgresql mysql",
3938
author="Catherine Devlin",

src/sql/column_guesser.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
class Column(list):
9-
"Store a column of tabular data; record its name and whether it is numeric"
9+
"""Store a column of tabular data; record its name and whether it is numeric"""
1010
is_quantity = True
1111
name = ""
1212

@@ -28,6 +28,9 @@ class ColumnGuesserMixin(object):
2828
pie: ... y
2929
"""
3030

31+
def __init__(self):
32+
self.keys = None
33+
3134
def _build_columns(self):
3235
self.columns = [Column() for col in self.keys]
3336
for row in self:

src/sql/connection.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
import re
2+
import traceback
33

44
import sqlalchemy
55

@@ -45,20 +45,21 @@ def __init__(self, connect_str=None, connect_args={}, creator=None):
4545
engine = sqlalchemy.create_engine(
4646
connect_str, connect_args=connect_args
4747
)
48-
except: # TODO: bare except; but what's an ArgumentError?
48+
except Exception as ex: # TODO: bare except; but what's an ArgumentError?
49+
print(traceback.format_exc())
4950
print(self.tell_format())
5051
raise
52+
self.url = engine.url
5153
self.dialect = engine.url.get_dialect()
52-
self.metadata = sqlalchemy.MetaData(bind=engine)
5354
self.name = self.assign_name(engine)
54-
self.session = engine.connect()
55-
self.connections[repr(self.metadata.bind.url)] = self
55+
self.internal_connection = engine.connect()
56+
self.connections[repr(self.url)] = self
5657
self.connect_args = connect_args
5758
Connection.current = self
5859

5960
@classmethod
6061
def set(cls, descriptor, displaycon, connect_args={}, creator=None):
61-
"Sets the current database connection"
62+
"""Sets the current database connection"""
6263

6364
if descriptor:
6465
if isinstance(descriptor, Connection):
@@ -94,16 +95,16 @@ def connection_list(cls):
9495
for key in sorted(cls.connections):
9596
engine_url = cls.connections[
9697
key
97-
].metadata.bind.url # type: sqlalchemy.engine.url.URL
98+
].url # type: sqlalchemy.engine.url.URL
9899
if cls.connections[key] == cls.current:
99100
template = " * {}"
100101
else:
101102
template = " {}"
102103
result.append(template.format(engine_url.__repr__()))
103104
return "\n".join(result)
104-
105+
105106
@classmethod
106-
def _close(cls, descriptor):
107+
def close(cls, descriptor):
107108
if isinstance(descriptor, Connection):
108109
conn = descriptor
109110
else:
@@ -115,8 +116,5 @@ def _close(cls, descriptor):
115116
"Could not close connection because it was not found amongst these: %s"
116117
% str(cls.connections.keys())
117118
)
118-
cls.connections.pop(str(conn.metadata.bind.url))
119-
conn.session.close()
120-
121-
def close(self):
122-
self.__class__._close(self)
119+
cls.connections.pop(str(conn.url))
120+
conn.internal_connection.close()

src/sql/magic.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
import re
3-
from string import Formatter
3+
import traceback
44

55
from IPython.core.magic import (
66
Magics,
@@ -10,7 +10,6 @@
1010
needs_local_scope,
1111
)
1212
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
13-
from IPython.display import display_javascript
1413
from sqlalchemy.exc import OperationalError, ProgrammingError, DatabaseError
1514

1615
import sql.connection
@@ -46,7 +45,8 @@ class SqlMagic(Magics, Configurable):
4645
style = Unicode(
4746
"DEFAULT",
4847
config=True,
49-
help="Set the table printing style to any of prettytable's defined styles (currently DEFAULT, MSWORD_FRIENDLY, PLAIN_COLUMNS, RANDOM)",
48+
help="Set the table printing style to any of prettytable's defined styles "
49+
"(currently DEFAULT, MSWORD_FRIENDLY, PLAIN_COLUMNS, RANDOM)",
5050
)
5151
short_errors = Bool(
5252
True,
@@ -72,17 +72,17 @@ class SqlMagic(Magics, Configurable):
7272
"odbc.ini",
7373
config=True,
7474
help="Path to DSN file. "
75-
"When the first argument is of the form [section], "
76-
"a sqlalchemy connection string is formed from the "
77-
"matching section in the DSN file.",
75+
"When the first argument is of the form [section], "
76+
"a sqlalchemy connection string is formed from the "
77+
"matching section in the DSN file.",
7878
)
7979
autocommit = Bool(True, config=True, help="Set autocommit mode")
8080

8181
def __init__(self, shell):
8282
Configurable.__init__(self, config=shell.config)
8383
Magics.__init__(self, shell=shell)
8484

85-
# Add ourself to the list of module configurable via %config
85+
# Add ourselves to the list of module configurable via %config
8686
self.shell.configurables.append(self)
8787

8888
@needs_local_scope
@@ -121,7 +121,7 @@ def __init__(self, shell):
121121
help="specify dictionary of connection arguments to pass to SQL driver",
122122
)
123123
@argument("-f", "--file", type=str, help="Run SQL from file at this path")
124-
def execute(self, line="", cell="", local_ns={}):
124+
def execute(self, line="", cell="", local_ns=None):
125125
"""Runs SQL statement against a database, specified by SQLAlchemy connect string.
126126
127127
If no database connection has been established, first word
@@ -147,15 +147,17 @@ def execute(self, line="", cell="", local_ns={}):
147147
148148
"""
149149
# Parse variables (words wrapped in {}) for %%sql magic (for %sql this is done automatically)
150+
if local_ns is None:
151+
local_ns = {}
150152
cell = self.shell.var_expand(cell)
151153
line = sql.parse.without_sql_comment(parser=self.execute.parser, line=line)
152154
args = parse_argstring(self.execute, line)
153155
if args.connections:
154156
return sql.connection.Connection.connections
155157
elif args.close:
156-
return sql.connection.Connection._close(args.close)
158+
return sql.connection.Connection.close(args.close)
157159

158-
# save globals and locals so they can be referenced in bind vars
160+
# save globals and locals, so they can be referenced in bind vars
159161
user_ns = self.shell.user_ns.copy()
160162
user_ns.update(local_ns)
161163

@@ -173,7 +175,7 @@ def execute(self, line="", cell="", local_ns={}):
173175

174176
if args.connection_arguments:
175177
try:
176-
# check for string deliniators, we need to strip them for json parse
178+
# check for string delineators, we need to strip them for json parse
177179
raw_args = args.connection_arguments
178180
if len(raw_args) > 1:
179181
targets = ['"', "'"]
@@ -183,7 +185,7 @@ def execute(self, line="", cell="", local_ns={}):
183185
raw_args = raw_args[1:-1]
184186
args.connection_arguments = json.loads(raw_args)
185187
except Exception as e:
186-
print(e)
188+
print(traceback.format_exc())
187189
raise e
188190
else:
189191
args.connection_arguments = {}
@@ -197,8 +199,10 @@ def execute(self, line="", cell="", local_ns={}):
197199
connect_args=args.connection_arguments,
198200
creator=args.creator,
199201
)
200-
except Exception as e:
201-
print(e)
202+
# Rollback just in case there was an error in previous statement
203+
conn.internal_connection.rollback()
204+
except Exception:
205+
print(traceback.format_exc())
202206
print(sql.connection.Connection.tell_format())
203207
return None
204208

@@ -220,7 +224,7 @@ def execute(self, line="", cell="", local_ns={}):
220224
and self.column_local_vars
221225
):
222226
# Instead of returning values, set variables directly in the
223-
# users namespace. Variable names given by column names
227+
# user's namespace. Variable names given by column names
224228

225229
if self.autopandas:
226230
keys = result.keys()
@@ -253,7 +257,8 @@ def execute(self, line="", cell="", local_ns={}):
253257
if self.short_errors:
254258
print(e)
255259
else:
256-
raise
260+
print(traceback.format_exc())
261+
raise e
257262

258263
legal_sql_identifier = re.compile(r"^[A-Za-z0-9#_$]+")
259264

@@ -279,7 +284,7 @@ def _persist_dataframe(self, raw, conn, user_ns, append=False):
279284
table_name = self.legal_sql_identifier.search(table_name).group(0)
280285

281286
if_exists = "append" if append else "fail"
282-
frame.to_sql(table_name, conn.session.engine, if_exists=if_exists)
287+
frame.to_sql(table_name, conn.internal_connection.engine, if_exists=if_exists)
283288
return "Persisted %s" % table_name
284289

285290

src/sql/parse.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import itertools
2-
import json
3-
import re
42
import shlex
53
from os.path import expandvars
64

7-
import six
85
from six.moves import configparser as CP
96
from sqlalchemy.engine.url import URL
107

@@ -13,11 +10,10 @@ def connection_from_dsn_section(section, config):
1310
parser = CP.ConfigParser()
1411
parser.read(config.dsn_filename)
1512
cfg_dict = dict(parser.items(section))
16-
return str(URL(**cfg_dict))
13+
return URL.create(**cfg_dict)
1714

1815

1916
def _connection_string(s, config):
20-
2117
s = expandvars(s) # for environment variables
2218
if "@" in s or "://" in s:
2319
return s

0 commit comments

Comments
 (0)