jbardin / scp.py

scp module for paramiko
Other
529 stars 139 forks source link

Pass progress callback when calling get/put methods on SCPClient #178

Open ScottCUSA opened 1 year ago

ScottCUSA commented 1 year ago

Hello,

I would like to be able to pass a progress callback when I am calling the get/put methods on SCPClient, like paramiko's SFTPClient class.

In my current use case, I cannot pass it on the SCPClient object constructor as a new progress bar object is initialized per file downloaded. To get around this I am setting, SCPClient._progress, before each put call, which isn't optimal.

Example:

    connection.open()
    scp_client = SCPClient(connection.client.get_transport())  # type: ignore

    for local_file in file_list:

        file_name = os.path.basename(local_file)
        print(file_name + ":")

        # initialize tqdm progress bar
        self._init_progress_bar()

        callback = self.progress_bar.update_to_from_scp if self.progress_bar is not None else None
        scp_client._progress = callback  # pylint: disable=protected-access

        scp_client.put(local_file, remote_dir)

        self._close_progress_bar()

Preferred API:

        scp_client.put(local_file, remote_dir, progress=callback)

I've removed the exception handling from this code for readability.

V/R Scott

remram44 commented 1 year ago

In practice there is no cost to creating SCPClients over and over, so you can put that in your loop. But I agree.

ScottCUSA commented 1 year ago

FYI: If you wanted to support 2, 3, and 4 argument callbacks with a single argument, it should be possible to use inspect.signature to determine the number of arguments in the provided Callable, and handle it accordingly.

Though if you're going to change the API it might also be a good idea to make it so the progress callbacks are consistent in order of arguments. Like so:

from inspect import signature, isfunction, ismethod

class TestClass():

    def method2(self, bytes_transfered, bytes_total):
        ...

    def method3(self, bytes_transfered, bytes_total, file_name):
        ...

    def method4(self, bytes_transfered, bytes_total, file_name, peer_name):
        ...

def vanilla_function2(bytes_transfered, bytes_total):
    ...
def vanilla_function3(bytes_transfered, bytes_total, file_name):
    ...
def vanilla_function4(bytes_transfered, bytes_total, file_name, peer_name):
    ...

classobj = TestClass()
classobj_method2_sig = signature(classobj.method2)
classobj_method3_sig = signature(classobj.method3)
classobj_method4_sig = signature(classobj.method4)
vanilla_function2_sig = signature(vanilla_function2)
vanilla_function3_sig = signature(vanilla_function3)
vanilla_function4_sig = signature(vanilla_function4)

print("\nSignatures: ")
print(f"classobj.method2: {classobj_method2_sig}")
print(f"classobj.method3: {classobj_method3_sig}")
print(f"classobj.method4: {classobj_method4_sig}")
print(f"vanilla_function2: {vanilla_function2_sig}")
print(f"vanilla_function3: {vanilla_function3_sig}")
print(f"vanilla_function4: {vanilla_function4_sig}")

print("\nNumber of Params")
print(f"len params classobj.method2: {len(classobj_method2_sig.parameters)}")
print(f"len params classobj.method3: {len(classobj_method3_sig.parameters)}")
print(f"len params classobj.method4: {len(classobj_method4_sig.parameters)}")
print(f"len params vanilla_function2: {len(vanilla_function2_sig.parameters)}")
print(f"len params vanilla_function3: {len(vanilla_function3_sig.parameters)}")
print(f"len params vanilla_function4: {len(vanilla_function4_sig.parameters)}")

print("\nMethod or Function:")
print(f"isfuction classobj.method2: {isfunction(classobj.method2)}")
print(f"ismethod classobj.method2: {ismethod(classobj.method2)}")
print(f"isfuction vanilla_function2: {isfunction(vanilla_function2)}")
print(f"ismethod vanilla_function2: {ismethod(vanilla_function2)}")
Signatures: 
classobj.method2: (bytes_transfered, bytes_total)
classobj.method3: (bytes_transfered, bytes_total, file_name)
classobj.method4: (bytes_transfered, bytes_total, file_name, peer_name)
vanilla_function2: (bytes_transfered, bytes_total)
vanilla_function3: (bytes_transfered, bytes_total, file_name)
vanilla_function4: (bytes_transfered, bytes_total, file_name, peer_name)

Number of Params
len params classobj.method2: 2
len params classobj.method3: 3
len params classobj.method4: 4
len params vanilla_function2: 2
len params vanilla_function3: 3
len params vanilla_function4: 4

Method or Function:
isfuction classobj.method2: False
ismethod classobj.method2: True
isfuction vanilla_function2: True
ismethod vanilla_function2: False
remram44 commented 1 year ago

FYI: If you wanted to support 2, 3, and 4 argument callbacks with a single argument, it should be possible to use inspect.signature to determine the number of arguments in the provided Callable, and handle it accordingly.

That seems brittle, it will break if someone uses *args to forward them to another function or whatever. A better option might be to pass an object with named fields.