# API design of library for argument inspection & filtering

I wrote the following Python library for getting a callable's argument names, testing whether a callable takes an argument with a given name, and filtering a dict down to just the keys that a callable accepts. You can see the entire code (tests etc.) on GitHub; only the actual module itself is shown below.

Aside from the usual things (code quality, unanticipated cases, etc.), I'd like some feedback on the design of the API of the library itself. Are the attributes & properties well-named, or are the names too unwieldy? Are the method names descriptive enough? Should the methods take kwargs as a dict or as **kwargs? Should missing() even take a dict or just a set of argument names? Should ArgSet store argument names in sets or frozensets (and, if the latter, should ArgSet itself be frozen)? And so forth.

"""
Simple callable argument inspection & filtering

argset provides a simple interface for determining whether a callable takes
an argument with a given name, filtering a dict of potential arguments down
to just those that a callable accepts, and determining any required arguments
that are missing from a dict of potential arguments.

"""

__version__ = "0.1.0"
__author__ = "John Thorvald Wodder II"
__author_email__ = "argset@varonathe.org"
__url__ = "https://github.com/jwodder/argset"

from dataclasses import dataclass
import inspect
from typing import Any, Callable, Dict, FrozenSet

__all__ = ["ArgSet", "argset"]

@dataclass
class ArgSet:
"""A representation of the arguments taken by a callable"""

#: The number of arguments that are positional-only and do not have default
#: values
required_positional_only: int

#: The number of arguments that are positional-only and have a default
#: value
optional_positional_only: int

#: The names of all positional-or-keyword or keyword-only arguments that do
#: not have default values
required_args: FrozenSet[str]

#: The names of all positional-or-keyword or keyword-only arguments that
#: have default values
optional_args: FrozenSet[str]

#: Whether the callable has an argument of the form *args
takes_args: bool

#: Whether the callable has an argument of the form **kwargs
takes_kwargs: bool

@property
def positional_only(self) -> int:
"""The total number of positional-only arguments"""
return self.required_positional_only + self.optional_positional_only

@property
def argnames(self) -> FrozenSet[str]:
"""The names of all positional-or-keyword or keyword-only arguments"""
return self.required_args | self.optional_args

def __contains__(self, arg: str) -> bool:
return (
self.takes_kwargs or arg in self.required_args or arg in self.optional_args
)

def select(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
"""
Returns all items in kwargs where the key is the name of a
positional-or-keyword or keyword-only argument accepted by the
callable.  If takes_kwargs is True, the return value is a copy of
kwargs.
"""
return {k: v for k, v in kwargs.items() if k in self}

def missing(self, kwargs: Dict[str, Any]) -> FrozenSet[str]:
"""
Returns all keys in required_args that do not appear in kwargs
"""
return frozenset(self.required_args - kwargs.keys())

def argset(func: Callable) -> ArgSet:
"""
Inspects a callable and returns a summary of its arguments as an ArgSet
"""
sig = inspect.signature(func)
required_pos = 0
optional_pos = 0
required_args = set()
optional_args = set()
takes_args = False
takes_kwargs = False
for param in sig.parameters.values():
if param.kind is param.POSITIONAL_ONLY:
if param.default is param.empty:
required_pos += 1
else:
optional_pos += 1
elif param.kind in (param.POSITIONAL_OR_KEYWORD, param.KEYWORD_ONLY):
if param.default is param.empty:
else:
elif param.kind is param.VAR_POSITIONAL:
takes_args = True
elif param.kind is param.VAR_KEYWORD:
takes_kwargs = True
else:
raise AssertionError(
"Unknown parameter type: {param.kind!r}"
)  # pragma: no cover
return ArgSet(
required_positional_only=required_pos,
optional_positional_only=optional_pos,
required_args=frozenset(required_args),
optional_args=frozenset(optional_args),
takes_args=takes_args,
takes_kwargs=takes_kwargs,
)
$$$$


argset() should be moved to a @classmethod pseudoconstructor in ArgSet`. Otherwise, the most concerning thing about this code is that it exists at all. I find that metaprogramming is a sign that something, somewhere, has gone wrong in the design process. It can be useful in certain narrow cases such as a Doxygen-like code-diagramming tool, but you've not indicated what the intended use case actually is.