Type Hinting with Decorator Removing First Argument

I’m trying to create a decorator that extracts the first argument from the function so it can be called later. The reason for this is we may call the function multiple times with only changes to the first argument.

I’m struggling get the typing scheme right where each function has the correct hints and does not drop argument names.

def model[T, R, **P](func: Callable[Concatenate[T, P], R]):
    def inner(*args: P.args, **kwargs: P.kwargs):
        def freq_dependent(f: T) -> R:
            result = func(f, *args, **kwargs)
            return np.broadcast_to(result, (f.shape[0], *result.shape[1:]))
        return freq_dependent
    return inner

@model
def resistor(f: np.ndarray, r: float, z0: float = 50) -> np.ndarray:
    y = 1/r
    return np.array([y, -y, -y, y]).reshape(1, 2, 2)

The hint for the resistor function is:

(r: float, z0: float = 50) -> ((f: T@model) -> R@model)

The hint for the return function is:

(f: T@model) -> R@model

I can also add the return type to the inner method like this:

    def inner(*args: P.args, **kwargs: P.kwargs) -> Callable[[T], R]:

But then I lose the argument names in the hints:

(r: float, z0: float = 50) -> ((ndarray) -> ndarray)

I don’t understand why it’s not resolving the “T” and “R” type variables. It seems like all the information is present to properly resolve the hint. Manually defining the return type for the inner function allows “T” and “R” to resolve, but then you lose the argument names which are arguably more important to the programmer.

Would the second argument need to have a default value just like the characteristic impedance of z0: float = 50 ohms since as you stated above only the frequency values via the array might need to change?

No the second argument should not have a default argument. This code is for a lightweight linear simulator. Characteristic impedance is almost always 50ohms, but the resistance is element dependent.

Really “z0” can be dropped because these functions are returning admittance parameters and not scattering parameters.

Below is a typical use-case.

att_circuit = SimpleCircuit();
r1 = resistor(17);
r2 = resistor(282);
att_circuit.add_element(r2, ('n1', 'gnd'));
att_circuit.add_element(r1, ('n1', 'n2'));
att_circuit.add_element(r2, ('n2', 'gnd'));

f1 = np.arange(0, 10e9, 100e6) + 100e6;
f2 = np.arange(0, 10e9, 10e6) + 10e6;

response_coarse = att_circuit.measure_s(f1, ('n1', 'n2'), z0=50);
response_fine = att_circuit.measure_s(f1, ('n1', 'n2'), z0=50);