Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ The semantic versioning only considers the public API as described in
paths are considered internals and can change in minor and patch releases.


v4.32.2 (2024-09-??)
--------------------

Fixed
^^^^^
- Callable type with subclass return not showing the ``--*.help`` option (`#567
<https://github.com/omni-us/jsonargparse/pull/567>`__).


v4.32.1 (2024-08-23)
--------------------

Expand Down
46 changes: 23 additions & 23 deletions jsonargparse/_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@
from ._optionals import get_config_read_mode
from ._type_checking import ArgumentParser
from ._util import (
NoneType,
Path,
argument_error,
change_to_path_dir,
default_config_option_help,
get_import_path,
get_typehint_origin,
import_object,
indent_text,
iter_to_set_str,
Expand Down Expand Up @@ -340,25 +338,21 @@
return instantiator_fn(self.basetype, **value)


class _ActionHelpClassPath(Action):

Check failure

Code scanning / CodeQL

Missing call to `__init__` during object initialization Error

Class _ActionHelpClassPath may not be initialized properly as
method Action.__init__
is not called from its
__init__ method
.
sub_add_kwargs: Dict[str, Any] = {}

def __init__(self, baseclass=None, **kwargs):
if baseclass is not None:
if get_typehint_origin(baseclass) == Union:
baseclasses = [c for c in baseclass.__args__ if c is not NoneType]
if len(baseclasses) == 1:
baseclass = baseclasses[0]
self._baseclass = baseclass
def __init__(self, typehint=None, **kwargs):
if typehint is not None:
self._typehint = typehint
else:
self._baseclass = kwargs.pop("_baseclass")
self._typehint = kwargs.pop("_typehint")
self.update_init_kwargs(kwargs)
super().__init__(**kwargs)

def update_init_kwargs(self, kwargs):
from ._typehints import get_subclasses_from_type
from ._typehints import get_subclass_names

self._basename = iter_to_set_str(get_subclasses_from_type(self._baseclass))
self._basename = iter_to_set_str(get_subclass_names(self._typehint, callable_return=True))
kwargs.update(
{
"metavar": "CLASS_PATH_OR_NAME",
Expand All @@ -369,26 +363,32 @@

def __call__(self, *args, **kwargs):
if len(args) == 0:
kwargs["_baseclass"] = self._baseclass
kwargs["_typehint"] = self._typehint
return type(self)(**kwargs)
dest = re.sub("\\.help$", "", self.dest)
return self.print_help(args, self._baseclass, dest)

def print_help(self, call_args, baseclass, dest):
from ._typehints import resolve_class_path_by_name
return self.print_help(args)

def print_help(self, call_args):
from ._typehints import (
ActionTypeHint,
get_optional_arg,
get_subclass_types,
get_unaliased_type,
resolve_class_path_by_name,
)

parser, _, value, option_string = call_args
try:
val_class = import_object(resolve_class_path_by_name(baseclass, value))
typehint = get_unaliased_type(get_optional_arg(self._typehint))
baseclasses = get_subclass_types(typehint, callable_return=True)
val_class = import_object(resolve_class_path_by_name(typehint, value))
except Exception as ex:
raise TypeError(f"{option_string}: {ex}") from ex
if get_typehint_origin(self._baseclass) == Union:
baseclasses = self._baseclass.__args__
else:
baseclasses = [baseclass]
if not any(is_subclass(val_class, b) for b in baseclasses):
raise TypeError(f'{option_string}: Class "{value}" is not a subclass of {self._basename}')
dest = re.sub("\\.help$", "", self.dest)
subparser = type(parser)(description=f"Help for {option_string}={get_import_path(val_class)}")
if ActionTypeHint.is_callable_typehint(typehint) and hasattr(typehint, "__args__"):
self.sub_add_kwargs["skip"] = {max(0, len(typehint.__args__) - 1)}
subparser.add_class_arguments(val_class, dest, **self.sub_add_kwargs)
remove_actions(subparser, (_HelpAction, _ActionPrintConfig, _ActionConfigLoad))
args = self.get_args_after_opt(parser.args)
Expand Down
2 changes: 1 addition & 1 deletion jsonargparse/_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def shtab_prepare_action(action, parser) -> None:
add_bash_typehint_completion(parser, action, message, choices)
choices = None
elif isinstance(action, _ActionHelpClassPath):
choices = get_help_class_choices(action._baseclass)
choices = get_help_class_choices(action._typehint)
if choices:
action.choices = choices

Expand Down
10 changes: 5 additions & 5 deletions jsonargparse/_parameter_resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ def __init__(self, resolver: str, data: Any) -> None:


def get_parameter_origins(component, parent) -> Optional[str]:
from ._typehints import get_subclasses_from_type, sequence_origin_types
from ._typehints import get_subclass_types, sequence_origin_types

if get_typehint_origin(component) in sequence_origin_types:
component = get_subclasses_from_type(component, names=False)
component = get_subclass_types(component, also_lists=True)
if isinstance(component, tuple):
assert parent is None or len(component) == len(parent)
return iter_to_set_str(get_parameter_origins(c, parent[n] if parent else None) for n, c in enumerate(component))
Expand Down Expand Up @@ -357,12 +357,12 @@ def is_param_subclass_instance_default(param: ParamData) -> bool:
from ._typehints import ActionTypeHint, get_optional_arg, get_subclass_types

annotation = get_optional_arg(param.annotation)
class_types = get_subclass_types(annotation)
class_types = get_subclass_types(annotation, callable_return=True)
return bool(
(class_types and isinstance(param.default, class_types))
or (
is_lambda(param.default)
and ActionTypeHint.is_callable_typehint(annotation, all_subtypes=False)
and ActionTypeHint.is_callable_typehint(annotation)
and getattr(annotation, "__args__", None)
and ActionTypeHint.is_subclass_typehint(annotation.__args__[-1], all_subtypes=False)
)
Expand Down Expand Up @@ -684,7 +684,7 @@ def replace_param_default_subclass_specs(self, params: List[ParamData]) -> None:
node = default_node.body
num_positionals = len(param.annotation.__args__) - 1
class_type = self.get_call_class_type(node)
subclass_types = get_subclass_types(param.annotation)
subclass_types = get_subclass_types(param.annotation, callable_return=True)
if not (class_type and subclass_types and is_subclass(class_type, subclass_types)):
continue
subclass_spec: dict = dict(class_path=get_import_path(class_type), init_args=dict())
Expand Down
4 changes: 2 additions & 2 deletions jsonargparse/_signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
ActionTypeHint,
LazyInitBaseClass,
callable_instances,
get_subclasses_from_type,
get_subclass_names,
is_optional,
)
from ._util import NoneType, get_private_kwargs, iter_to_set_str
Expand Down Expand Up @@ -541,7 +541,7 @@ def add_subclass_arguments(
if skip is not None:
skip = {f"{nested_key}.init_args." + s for s in skip}
param = ParamData(name=nested_key, annotation=Union[baseclass], component=baseclass)
str_baseclass = iter_to_set_str(get_subclasses_from_type(param.annotation))
str_baseclass = iter_to_set_str(get_subclass_names(param.annotation))
kwargs.update(
{
"metavar": metavar,
Expand Down
108 changes: 54 additions & 54 deletions jsonargparse/_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,11 @@ def prepare_add_argument(args, kwargs, enable_path, container, logger, sub_add_k
typehint = kwargs.pop("type")
if args[0].startswith("--") and ActionTypeHint.supports_append(typehint):
args = tuple(list(args) + [args[0] + "+"])
if ActionTypeHint.is_subclass_typehint(typehint, all_subtypes=False):
if ActionTypeHint.is_subclass_typehint(
typehint, all_subtypes=False
) or ActionTypeHint.is_return_subclass_typehint(typehint):
help_option = f"--{args[0]}.help" if args[0][0] != "-" else f"{args[0]}.help"
help_action = container.add_argument(help_option, action=_ActionHelpClassPath(baseclass=typehint))
help_action = container.add_argument(help_option, action=_ActionHelpClassPath(typehint=typehint))
if sub_add_kwargs:
help_action.sub_add_kwargs = sub_add_kwargs
kwargs["action"] = ActionTypeHint(typehint=typehint, enable_path=enable_path, logger=logger)
Expand Down Expand Up @@ -303,15 +305,7 @@ def is_subclass_typehint(typehint, all_subtypes=True, also_lists=False):
test = all if all_subtypes else any
k = {"also_lists": also_lists}
return test(ActionTypeHint.is_subclass_typehint(s, **k) for s in subtypes)
return (
inspect.isclass(typehint)
and typehint not in leaf_or_root_types
and not get_registered_type(typehint)
and not is_pydantic_type(typehint)
and not is_dataclass_like(typehint)
and typehint_origin is None
and not is_subclass(typehint, (Path, Enum))
)
return is_single_subclass_typehint(typehint, typehint_origin)

@staticmethod
def is_return_subclass_typehint(typehint):
Expand All @@ -336,14 +330,10 @@ def is_mapping_typehint(typehint):
return False

@staticmethod
def is_callable_typehint(typehint, all_subtypes=True):
typehint = get_unaliased_type(typehint)
typehint_origin = get_typehint_origin(typehint)
if typehint_origin == Union:
subtypes = [a for a in typehint.__args__ if a != NoneType]
test = all if all_subtypes else any
return test(ActionTypeHint.is_callable_typehint(s) for s in subtypes)
return typehint_origin in callable_origin_types or typehint in callable_origin_types
def is_callable_typehint(typehint):
typehint = typehint_from_action(typehint)
typehint_origin = get_typehint_origin(get_optional_arg(get_unaliased_type(typehint)))
return typehint_origin in callable_origin_types

def is_init_arg_mapping_typehint(self, key, cfg):
result = False
Expand Down Expand Up @@ -635,10 +625,13 @@ def get_class_parser(val_class, sub_add_kwargs=None, skip_args=0):

def extra_help(self):
extra = ""
if self.is_subclass_typehint(self, all_subtypes=False) or get_typehint_origin(
self._typehint
typehint = get_optional_arg(self._typehint)
if self.is_subclass_typehint(typehint, all_subtypes=False) or get_typehint_origin(
typehint
) in callable_origin_types.union({Type, type}):
class_paths = get_all_subclass_paths(self._typehint)
if self.is_callable_typehint(typehint) and getattr(typehint, "__args__", None):
typehint = get_callable_return_type(get_optional_arg(typehint))
class_paths = get_all_subclass_paths(typehint)
if class_paths:
extra = ", known subclasses: " + ", ".join(class_paths)
return extra
Expand Down Expand Up @@ -680,22 +673,6 @@ def is_pathlike(typehint) -> bool:
return is_subclass(typehint, os.PathLike)


def get_subclasses_from_type(typehint, names=True, subclasses=None) -> tuple:
if subclasses is None:
subclasses = []
origin = get_typehint_origin(typehint)
if origin == Union or origin in sequence_origin_types:
for subtype in typehint.__args__:
get_subclasses_from_type(subtype, names, subclasses)
elif ActionTypeHint.is_subclass_typehint(typehint, all_subtypes=False):
if names:
if typehint.__name__ not in subclasses:
subclasses.append(typehint.__name__)
elif typehint not in subclasses:
subclasses.append(typehint)
return tuple(subclasses)


def raise_unexpected_value(message: str, val: Any = inspect._empty, exception: Optional[Exception] = None) -> NoReturn:
if val is not inspect._empty:
message += f". Got value: {val}"
Expand Down Expand Up @@ -1143,29 +1120,52 @@ def get_callable_return_type(typehint):
return return_type


def get_subclass_types(typehint, callable_return=True):
subclass_types = None
if (
callable_return
and ActionTypeHint.is_callable_typehint(typehint, all_subtypes=False)
and getattr(typehint, "__args__", None)
):
typehint = get_optional_arg(typehint)
typehint = typehint.__args__[-1]
if ActionTypeHint.is_subclass_typehint(typehint, all_subtypes=False):
if get_typehint_origin(typehint) == Union:
subclass_types = tuple(t for t in typehint.__args__ if ActionTypeHint.is_subclass_typehint(t))
else:
subclass_types = (typehint,)
return subclass_types
def is_single_subclass_typehint(typehint, typehint_origin):
return (
inspect.isclass(typehint)
and typehint not in leaf_or_root_types
and not get_registered_type(typehint)
and not is_pydantic_type(typehint)
and not is_dataclass_like(typehint)
and typehint_origin is None
and not is_subclass(typehint, (Path, Enum))
)


def yield_subclass_types(typehint, also_lists=False, callable_return=False):
typehint = typehint_from_action(typehint)
if typehint is None:
return
typehint = get_unaliased_type(get_optional_arg(get_unaliased_type(typehint)))
typehint_origin = get_typehint_origin(typehint)
if callable_return and typehint_origin in callable_origin_types:
return_type = get_callable_return_type(typehint)
if return_type:
k = {"also_lists": also_lists, "callable_return": callable_return}
yield from yield_subclass_types(return_type, **k)
elif typehint_origin == Union or (also_lists and typehint_origin in sequence_origin_types):
k = {"also_lists": also_lists, "callable_return": callable_return}
for subtype in typehint.__args__:
yield from yield_subclass_types(subtype, **k)
if is_single_subclass_typehint(typehint, typehint_origin):
yield typehint


def get_subclass_types(typehint, also_lists=False, callable_return=False):
types = tuple(yield_subclass_types(typehint, also_lists=also_lists, callable_return=callable_return))
return types or None


def get_subclass_names(typehint, callable_return=False):
return tuple(t.__name__ for t in yield_subclass_types(typehint, callable_return=callable_return))


def adapt_partial_callable_class(callable_type, subclass_spec):
partial_classes = False
num_partial_args = 0
return_type = get_callable_return_type(callable_type)
if return_type:
subclass_types = get_subclass_types(return_type, callable_return=False)
subclass_types = get_subclass_types(return_type)
class_type = import_object(resolve_class_path_by_name(return_type, subclass_spec.class_path))
if subclass_types and is_subclass(class_type, subclass_types):
subclass_spec = subclass_spec.clone()
Expand Down
2 changes: 1 addition & 1 deletion jsonargparse_tests/test_shtab.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ def test_bash_nested_subclasses(parser, subtests):
def test_bash_callable_return_class(parser, subtests):
parser.add_argument("--cls", type=Callable[[int], Base])
shtab_script = get_shtab_script(parser, "bash")
assert "_option_strings=('-h' '--help' '--cls' '--cls.p2' '--cls.p3')" in shtab_script
assert "_option_strings=('-h' '--help' '--cls.help' '--cls' '--cls.p2' '--cls.p3')" in shtab_script
assert "--cls.p1" not in shtab_script
classes = f"{__name__}.Base {__name__}.SubA {__name__}.SubB".split()
assert_bash_typehint_completions(
Expand Down
Loading