From 665b8372faa756488f240374d7da0e2ab88158ea Mon Sep 17 00:00:00 2001 From: Haitao Zheng <203365637+0xTaoZ@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:09:20 +0200 Subject: [PATCH] fix(api): omit variadic kwargs from auto labels --- src/pyinfra/api/operation.py | 14 +++++++++++--- tests/test_api/test_api_operations.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/pyinfra/api/operation.py b/src/pyinfra/api/operation.py index 57ebe2804..8b6dd31a6 100644 --- a/src/pyinfra/api/operation.py +++ b/src/pyinfra/api/operation.py @@ -8,7 +8,7 @@ from __future__ import annotations from functools import wraps -from inspect import signature +from inspect import Parameter, signature from io import StringIO from types import FunctionType from typing import TYPE_CHECKING, Any, cast @@ -300,7 +300,7 @@ def decorated_func(*args: P.args, **kwargs: P.kwargs) -> OperationMeta: # Attach normal args, if we're auto-naming this operation if add_args: - op_meta = attach_args(op_meta, args, kwargs) + op_meta = attach_args(op_meta, func, args, kwargs) # Check if we're actually running the operation on this host # Run once and we've already added meta for this op? Stop here. @@ -488,13 +488,21 @@ def _get_arg_value(arg): return arg -def attach_args(op_meta, args, kwargs): +def attach_args(op_meta, func, args, kwargs): for arg in args: if arg not in op_meta.args: op_meta.args.append(str(_get_arg_value(arg))) # Attach keyword args + named_parameters = { + key + for key, parameter in signature(func).parameters.items() + if parameter.kind is not Parameter.VAR_KEYWORD + } for key, value in kwargs.items(): + if key not in named_parameters: + continue + arg = "=".join((str(key), str(_get_arg_value(value)))) if arg not in op_meta.args: op_meta.args.append(arg) diff --git a/tests/test_api/test_api_operations.py b/tests/test_api/test_api_operations.py index a8237f9c5..b845df24d 100644 --- a/tests/test_api/test_api_operations.py +++ b/tests/test_api/test_api_operations.py @@ -1,4 +1,5 @@ from collections import defaultdict +from io import StringIO from os import path from unittest import TestCase from unittest.mock import mock_open, patch @@ -118,6 +119,29 @@ def test_op(self): disconnect_all(state) + def test_template_op_auto_name_does_not_include_data_kwargs(self): + inventory = make_inventory() + state = State(inventory, Config()) + state.current_stage = StateStage.Prepare + connect_all(state) + + add_op( + state, + files.template, + StringIO("password={{ password }}"), + "/etc/config.toml", + password="!CLEARTEXTPASSWORD", + ) + + [op_hash] = state.get_op_order() + op_args = state.op_meta[op_hash].args + + assert any(arg.startswith("StringIO(hash=") for arg in op_args) + assert "/etc/config.toml" in op_args + assert "password=!CLEARTEXTPASSWORD" not in op_args + + disconnect_all(state) + @patch("pyinfra.api.util.open", mock_open(read_data="test!"), create=True) @patch("pyinfra.operations.files.os.path.isfile", lambda *args, **kwargs: True) def test_file_upload_op(self):