Skip to content

Commit 030b930

Browse files
committed
feat: Allow transport objects to be closed
The base transport class now has a close method, which marks the transport as closed and additionally will throw an exception if the call method is called on a closed transport object. In addition, the close method will be called by __del__, so the transport will be automatically closed prior to it being destructed. Transports should override the _close method if they need to free any transport-specific resources (currently only DatabaseTransport and SshTransport need to do so). Fixes #64
1 parent f00770c commit 030b930

10 files changed

+107
-5
lines changed

src/itoolkit/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .itoolkit import iSqlFree
1717
from .itoolkit import iSqlParm
1818
from .itoolkit import iXml
19+
from .errors import TransportClosedException
1920

2021
__all__ = [
2122
'iToolKit',
@@ -34,4 +35,5 @@
3435
'iSqlParm',
3536
'iSqlFree',
3637
'iXml',
38+
'TransportClosedException',
3739
]

src/itoolkit/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
__all__ = [
2+
'TransportClosedException',
3+
]
4+
5+
6+
class TransportClosedException(Exception):
7+
"""Raised when an operation is performed on a closed transport"""
8+
pass

src/itoolkit/itoolkit.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,9 @@ def call(self, itrans):
11581158
11591159
Returns:
11601160
none
1161+
1162+
Raises:
1163+
TransportClosedException: If the transport has been closed.
11611164
"""
11621165
if self.trace_fd:
11631166
self.trace_write('***********************')

src/itoolkit/transport/base.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from ..errors import TransportClosedException
2+
3+
14
class XmlServiceTransport(object):
25
"""XMLSERVICE transport base class
36
@@ -12,6 +15,10 @@ def __init__(self, ctl="*here *cdata", ipc="*na"):
1215
self.ctl = ctl
1316

1417
self.trace_attrs = ["ipc", "ctl"]
18+
self._is_open = True
19+
20+
def __del__(self):
21+
self.close()
1522

1623
def trace_data(self):
1724
output = ""
@@ -35,10 +42,34 @@ def call(self, tk):
3542
Returns:
3643
str: The XML returned from XMLSERVICE
3744
"""
45+
self._ensure_open()
46+
3847
return self._call(tk)
3948

4049
def _call(self, tk):
4150
"""Called by :py:func:`call`. This should be overridden by subclasses
4251
to the call function instead of overriding :py:func:`call` directly.
4352
"""
4453
raise NotImplementedError
54+
55+
def _ensure_open(self):
56+
"""This should be called by any subclass function which uses
57+
resources which may have been released when `close` is called."""
58+
if not self._is_open:
59+
raise TransportClosedException()
60+
61+
def close(self):
62+
"""Close the connection now rather than when :py:func:`__del__` is
63+
called.
64+
65+
The transport will be unusable from this point forward and a
66+
:py:exc:`TransportClosedException` exception will be raised if any
67+
operation is attempted with the transport.
68+
"""
69+
self._close()
70+
self._is_open = False
71+
72+
def _close(self):
73+
"""Called by `close`. This should be overridden by subclasses to close
74+
any resources specific to that implementation."""
75+
pass

src/itoolkit/transport/database.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,6 @@ def _call(self, tk):
7777
getattr(cursor, self.func)(self.query, parms)
7878

7979
return "".join(row[0] for row in cursor).rstrip('\0')
80+
81+
def _close(self):
82+
self.conn.close()

src/itoolkit/transport/ssh.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,6 @@ def _call(self, tk):
103103
stdout.channel.close()
104104
stderr.channel.close()
105105
return xml_out
106+
107+
def _close(self):
108+
self.conn.close()

tests/test_unit_transport_database.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from itoolkit import iToolKit
2-
from itoolkit.transport import DatabaseTransport
1+
import pytest
32

3+
from itoolkit import iToolKit, TransportClosedException
4+
from itoolkit.transport import DatabaseTransport
45

56
def test_database_transport_callproc(database_callproc):
67
transport = DatabaseTransport(database_callproc)
@@ -60,3 +61,13 @@ def test_database_transport_callproc_schema(database_execute):
6061

6162
assert len(cursor.execute.call_args[0]) > 0
6263
assert schema in cursor.execute.call_args[0][0]
64+
65+
66+
def test_database_transport_call_raises_when_closed(database_execute):
67+
schema = 'MYSCHEMA'
68+
transport = DatabaseTransport(database_execute, schema=schema)
69+
transport.close()
70+
71+
with pytest.raises(TransportClosedException):
72+
tk = iToolKit()
73+
out = transport.call(tk)

tests/test_unit_transport_direct.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
import sys
33

4-
from itoolkit import iToolKit
4+
from itoolkit import iToolKit, TransportClosedException
55
from itoolkit.transport import DirectTransport
66
import itoolkit.transport.direct
77

@@ -63,3 +63,14 @@ def test_direct_transport(mocker):
6363

6464
assert_xmlservice_params_correct(mock)
6565
assert isinstance(out, (bytes, str))
66+
67+
68+
def test_direct_transport_call_raises_when_closed(mocker):
69+
mock = mock_direct(mocker)
70+
71+
transport = DirectTransport()
72+
transport.close()
73+
74+
with pytest.raises(TransportClosedException):
75+
tk = iToolKit()
76+
out = transport.call(tk)

tests/test_unit_transport_http.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import pytest
12
import sys
23

3-
from itoolkit import iToolKit
4+
from itoolkit import iToolKit, TransportClosedException
45
from itoolkit.transport import HttpTransport
56

67
if sys.version_info >= (3, 0):
@@ -104,3 +105,19 @@ def test_http_transport_with_database(mocker):
104105
pwd=password,
105106
db2=database
106107
)
108+
109+
110+
def test_http_transport_call_raises_when_closed(mocker):
111+
mock_urlopen = mock_http_urlopen(mocker)
112+
113+
url = 'http://example.com/cgi-bin/xmlcgi.pgm'
114+
user = 'dummy'
115+
password = 'passw0rd'
116+
database = 'MYDB'
117+
118+
transport = HttpTransport(url, user, password, database=database)
119+
transport.close()
120+
121+
with pytest.raises(TransportClosedException):
122+
tk = iToolKit()
123+
out = transport.call(tk)

tests/test_unit_transport_ssh.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from itoolkit import iToolKit
1+
import pytest
2+
3+
from itoolkit import iToolKit, TransportClosedException
24
from itoolkit.transport import SshTransport
35

46

@@ -29,3 +31,14 @@ def test_ssh_transport_minimal(mocker):
2931

3032
command = "/QOpenSys/pkgs/bin/xmlservice-cli"
3133
ssh_client.exec_command.assert_called_once_with(command)
34+
35+
36+
def test_ssh_transport_raises_when_closed(mocker):
37+
ssh_client = mock_ssh(mocker)
38+
39+
transport = SshTransport(ssh_client)
40+
transport.close()
41+
42+
with pytest.raises(TransportClosedException):
43+
tk = iToolKit()
44+
out = transport.call(tk)

0 commit comments

Comments
 (0)