Skip to content

Commit 076b5ed

Browse files
committed
implemented property handling for MultiConnection
1 parent d55f964 commit 076b5ed

File tree

4 files changed

+160
-11
lines changed

4 files changed

+160
-11
lines changed

ssh_utilities/multi_connection/_delegated.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,28 @@ def __init__(self, method_name: str, parent_ref: "ABCs",
4848
self._method_name = method_name
4949
self._parent_ref = parent_ref
5050
self._inner_class = inner_class
51+
self._property = False
52+
53+
def is_property(self):
54+
"""Set this callabel to behave like a property."""
55+
self._property = True
5156

5257
def __call__(self, *args, **kwargs):
53-
"""Call the appropriate method on all connections asynchronouslly."""
58+
"""Call the appropriate method on all connections asynchronously."""
5459
iterables = [(c, *args, *kwargs)
5560
for c in self._parent_ref.mc.values()]
5661
return self._parent_ref.mc.pool.map(self._call, *zip(*iterables))
5762

5863
def _call(self, c: "_CONN", *args, **kwargs):
5964
"""Make method calls for each of the connections."""
6065
method = getattr(getattr(c, self._inner_class), self._method_name)
61-
return method(*args, **kwargs)
66+
if self._property:
67+
return method
68+
else:
69+
return method(*args, **kwargs)
6270

6371

72+
# ! properties
6473
# TODO what about abstract properties delegation?
6574
# TODO mayybe this is too deep magic? still not sure if we are returning
6675
# TODO actual class or class instance, this can have dnagendous consequences!
@@ -77,7 +86,7 @@ def Inner(abc_parent: "ABCs", multi_connection: "MultiConnection"): # NOSONAR
7786
>>> method1[Connection1, Connection2]
7887
>>> method2[Connection1, Connection2]
7988
80-
The methods became classes and each 'method' holds reference to all
89+
The methods become classes and each one holds reference to all
8190
open connections
8291
8392
Parameters
@@ -96,8 +105,15 @@ def Inner(abc_parent: "ABCs", multi_connection: "MultiConnection"): # NOSONAR
96105
for name in abc_parent.__abstractmethods__:
97106
if hasattr(abc_parent, name):
98107
inner_class = abc_parent.__name__.replace("ABC", "").lower()
99-
setattr(abc_parent, name, delegated(name, abc_parent,
100-
inner_class))
108+
attr = getattr(abc_parent, name)
109+
new_method = delegated(name, abc_parent, inner_class)
110+
if isinstance(attr, property):
111+
new_method.is_property()
112+
new_method = property(new_method.__call__,
113+
attr.__set__, attr.__delattr__)
114+
115+
setattr(abc_parent, name, new_method)
116+
101117
implemented.add(name)
102118
abc_parent.__abstractmethods__ = frozenset(
103119
abc_parent.__abstractmethods__ - implemented

ssh_utilities/multi_connection/_dict_interface.py

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"""module providing dictionary interface for multi_connection."""
22

33
import logging
4+
from copy import copy
45
from typing import (TYPE_CHECKING, Dict, ItemsView, KeysView, Optional, Tuple,
5-
Union, ValuesView)
6+
Union, ValuesView, TypeVar)
67

78
if TYPE_CHECKING:
89
from ..local import LocalConnection
910
from ..remote import SSHConnection
1011
from .multi_connection import MultiConnection
1112

1213
_CONN = Union[SSHConnection, LocalConnection]
13-
14+
_DictInterface1 = TypeVar("_DictInterface1")
1415

1516
log = logging.getLogger(__name__)
1617

@@ -47,46 +48,151 @@ def __len__(self) -> int:
4748
return len(self._connections)
4849

4950
def keys(self) -> KeysView[str]:
51+
"""Iterate over registered key same as `dict.keys` method.
52+
53+
Yields
54+
------
55+
KeysView[str]
56+
key
57+
"""
5058
return self._connections.keys()
5159

5260
def values(self) -> ValuesView["_CONN"]:
61+
"""Iterate over registered Connections same as `dict.values` method.
62+
63+
Yields
64+
------
65+
ValuesView[_CONN]
66+
`SSHConnection`/`LocalConnection`
67+
"""
5368
return self._connections.values()
5469

5570
def items(self) -> ItemsView[str, "_CONN"]:
71+
"""Iterate over key, Connection pairs same as `dict.items` method.
72+
73+
Yields
74+
------
75+
ItemsView[str, _CONN]
76+
key, `SSHConnection`/`LocalConnection`
77+
"""
5678
return self._connections.items()
5779

5880
def pop(self, key: str, *args) -> "_CONN":
81+
"""Pop one connection from `MultiConnection` object based on key.
82+
83+
Parameters
84+
----------
85+
key: str
86+
key denoting the connection
87+
default: Any, optional
88+
optianal value returned if key is not pressent
89+
90+
Returns
91+
-------
92+
_CONN
93+
`SSHConnection` or `LocalConnection` object
94+
95+
Raises
96+
------
97+
AttributeError
98+
if the input key is not present among the connections and default
99+
is not defined.
100+
"""
59101
conn = self._connections.pop(key, *args)
60102
self._share_connection.pop(key, *args)
61103
return conn
62104

63105
def popitem(self) -> Tuple[str, "_CONN"]:
106+
"""Pops one key, connection pair from `MultiConnection`.
107+
108+
Returns
109+
-------
110+
Tuple[str, _CONN]
111+
key, `SSHConnection`/`LocalConnection` pair
112+
"""
64113
key, conn = self._connections.popitem()
65114
self._share_connection.pop(key, None)
66115
return key, conn
67116

68117
def update(self, other: "MultiConnection"):
118+
"""Updates `Multiconnection` with another `Multiconnection`.
119+
120+
This only merges underlying dictionaries holding connections
121+
122+
Parameters
123+
----------
124+
other : `MultiConnection`
125+
the added object of same type as self
126+
"""
69127
self._add_multi(other)
70128

71129
def get(self, key: str, *args) -> Optional["_CONN"]:
130+
"""Get one connection from `MultiConnection` object based on key.
131+
132+
Parameters
133+
----------
134+
key: str
135+
key denoting the connection
136+
default: Any, optional
137+
optianal value returned if key is not pressent
138+
139+
Returns
140+
-------
141+
_CONN
142+
`SSHConnection` or `LocalConnection` object shallow copy
143+
144+
Raises
145+
------
146+
AttributeError
147+
if the input key is not present among the connections and default
148+
is not defined.
149+
"""
72150
return self._connections.get(key, *args)
73151

74-
def copy(self):
75-
return self
152+
def copy(self: "_DictInterface1") -> "_DictInterface1":
153+
"""Get a shallow copy of `MultiConnection`.
154+
155+
Returns
156+
-------
157+
`MultiConnection`
158+
MultiConnection object shallow copy
159+
"""
160+
return copy(self)
76161

77162
def clear(self):
163+
"""Close and delete all underlying connections."""
78164
self.close()
79165
self._connections.clear()
80166
self._share_connection.clear()
81167
self.pool.shutdown()
82168

83169
def _add_one(self, other: "_CONN",
84170
key: Optional[str] = None):
85-
171+
"""Add one connecction to the undelying dictionary.
172+
173+
Parameters
174+
----------
175+
other: _CONN
176+
`SSHConnection` or `LocalConnection`
177+
key: Optional[str]
178+
if used connection is added under this key, else key is extracted
179+
from `connection.server_name` attribute
180+
181+
Raises
182+
------
183+
AttributeError
184+
if key is already pressent among registered connections
185+
"""
86186
if key is None:
87187
key = other.server_name.lower()
188+
189+
if key in self.keys():
190+
raise KeyError(f"Cannot register new Connection under key: {key}, "
191+
f"change Connection.server_name attribute or pass "
192+
f"in another key")
88193
self._connections.update({key: other})
89194

90195
def _add_multi(self, other: "MultiConnection"):
196+
"""Register multiple new connections."""
91197
self._share_connection.update(other._share_connection)
92198
self._connections.update(other._connections)

ssh_utilities/multi_connection/_persistence.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@ def __repr__(self):
4646
self.__str__()
4747

4848
def __deepcopy__(self, memodict: dict = {}):
49+
"""On deepcopy we create new instance as this is simpler and safer."""
4950
return MultiConnection.from_dict(self.to_dict())
5051

5152
def __getstate__(self):
53+
"""Gets the state of object for pickling."""
5254
return self.to_dict()
5355

5456
def __setstate__(self, state: dict):
55-
57+
"""Initializes the object after load from pickle."""
5658
ssh_servers, share_connection, local, thread_safe = (
5759
self._parse_persistence_dict(state)
5860
)
@@ -63,6 +65,13 @@ def __setstate__(self, state: dict):
6365

6466
def to_dict(self) -> Dict[int, Dict[str, Optional[Union[str, bool,
6567
int, None]]]]:
68+
"""Saves all the importatnt info from object to dictonary.
69+
70+
Returns
71+
-------
72+
Dict[int, Dict[str, Optional[Union[str, bool, int, None]]]]
73+
dictionary representing the object
74+
"""
6675
conns = {}
6776
for i, (v, sc) in enumerate(zip(self._connections.values(),
6877
self._share_connection.values())):
@@ -75,7 +84,18 @@ def to_dict(self) -> Dict[int, Dict[str, Optional[Union[str, bool,
7584
@staticmethod
7685
def _parse_persistence_dict(d: dict) -> Tuple[List[str], List[int],
7786
List[bool], List[bool]]:
87+
"""Parses dictionary produced by `to_dict` method.
7888
89+
Parameters
90+
----------
91+
d : dict
92+
dictionary of values needed to reinitialize the class
93+
94+
Returns
95+
-------
96+
Tuple[List[str], List[int], List[bool], List[bool]]
97+
Tuple of lists with parsed information
98+
"""
7999
share_connection = []
80100
ssh_servers = []
81101
local = []

tests/manual_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
for out in mc.os.isfile("/home/rynik/hw_config_Kohn.log"):
66
print(out)
77

8+
for n in mc.os.name:
9+
print(n)
10+
11+
for p in mc.os.path.realpath("/home/rynik"):
12+
print(p)
13+
814
a = mc.to_dict()
915
del mc
1016

17+
1118
with MultiConnection(["kohn"], quiet=True) as mc:
1219
for out in mc.os.isfile("/home/rynik/hw_config_Kohn.log"):
1320
print(out)

0 commit comments

Comments
 (0)