duit.arguments.Arguments

  1import argparse
  2from collections import defaultdict
  3from typing import Any, List
  4
  5from duit.annotation.AnnotationFinder import AnnotationFinder
  6from duit.arguments.Argument import Argument
  7from duit.arguments.adapters.BaseTypeAdapter import BaseTypeAdapter
  8from duit.arguments.adapters.BooleanTypeAdapter import BooleanTypeAdapter
  9from duit.arguments.adapters.DefaultTypeAdapter import DefaultTypeAdapter
 10from duit.arguments.adapters.EnumTypeAdapter import EnumTypeAdapter
 11from duit.arguments.adapters.PathTypeAdapter import PathTypeAdapter
 12from duit.arguments.adapters.VectorTypeAdapter import VectorTypeAdapter
 13from duit.model.DataField import DataField
 14
 15
 16class Arguments:
 17    """
 18    A class for handling command-line arguments based on annotations in objects.
 19
 20    Attributes:
 21        type_adapters (List[BaseTypeAdapter]): A list of type adapters to handle specific data types.
 22        default_serializer (BaseTypeAdapter): The default type adapter for serialization.
 23        _annotation_finder (AnnotationFinder): An instance of AnnotationFinder for finding Argument annotations in objects.
 24    """
 25
 26    def __init__(self):
 27        """
 28        Initialize an Arguments instance with default configuration.
 29        """
 30        self.type_adapters: List[BaseTypeAdapter] = [
 31            BooleanTypeAdapter(),
 32            EnumTypeAdapter(),
 33            VectorTypeAdapter(),
 34            PathTypeAdapter()
 35        ]
 36        self.default_serializer: BaseTypeAdapter = DefaultTypeAdapter()
 37
 38        # setup annotation finder
 39        def _is_field_valid(field: DataField, annotation: Argument):
 40            if callable(field.value):
 41                return False
 42            return True
 43
 44        self._annotation_finder: AnnotationFinder[Argument] = AnnotationFinder(Argument, _is_field_valid,
 45                                                                               recursive=True)
 46
 47    def add_and_configure(self,
 48                          parser: argparse.ArgumentParser,
 49                          obj: Any,
 50                          use_attribute_path_as_name: bool = False) -> argparse.Namespace:
 51        """
 52        Add command-line arguments to the parser and configure them based on annotations in the object.
 53
 54        Args:
 55            parser (argparse.ArgumentParser): The argparse parser to which the arguments will be added.
 56            obj (Any): The object containing annotations for command-line arguments.
 57            use_attribute_path_as_name (bool): Use the attribute path as name. This allows nested attributes share the same name.
 58
 59        Returns:
 60            argparse.Namespace: The parsed namespace containing the configured command-line arguments.
 61        """
 62        self.add_arguments(parser, obj, use_attribute_path_as_name)
 63        args = parser.parse_args()
 64        self.configure(args, obj)
 65        return args
 66
 67    def add_arguments(self,
 68                      parser: argparse.ArgumentParser, obj: Any,
 69                      use_attribute_path_as_name: bool = False):
 70        """
 71        Add command-line arguments to the parser based on annotations in the object.
 72
 73        Args:
 74            parser (argparse.ArgumentParser): The argparse parser to which the arguments will be added.
 75            obj (Any): The object containing annotations for command-line arguments.
 76            use_attribute_path_as_name (bool): Use the attribute path as name. This allows nested attributes share the same name.
 77        """
 78        groups = defaultdict(list)
 79
 80        for attribute_identifier, (field, argument) in self._annotation_finder.find_with_identifier(obj).items():
 81            if argument.dest is None:
 82                attribute_name = attribute_identifier.path if use_attribute_path_as_name else attribute_identifier.name
 83                argument.dest = f"--{self.to_argument_str(attribute_name)}"
 84
 85            groups[argument.group].append((field, argument))
 86
 87        group_keys = sorted(groups.keys(), key=lambda x: (x is not None, x))
 88        if None in group_keys:
 89            group_keys.remove(None)
 90            group_keys.insert(0, None)
 91
 92        parser_groups = {g: parser.add_argument_group(g) for g in group_keys if g is not None}
 93        parser_groups[None] = parser
 94
 95        for key in group_keys:
 96            p = parser_groups[key]
 97
 98            for field, argument in groups[key]:
 99                type_adapter = self._get_matching_type_adapter(field)
100                type_adapter.add_argument(p, argument, field.value)
101
102    def configure(self, args: argparse.Namespace, obj: Any):
103        """
104        Configure object fields based on the parsed command-line arguments.
105
106        Args:
107            args (argparse.Namespace): The parsed namespace containing the command-line arguments.
108            obj (Any): The object containing annotations for command-line arguments.
109        """
110        for name, (field, argument) in self._annotation_finder.find(obj).items():
111            dest = name if argument.dest is None else argument.dest
112            ns_dest = self.to_namespace_str(dest)
113
114            # only override if CLI actually provided it
115            if not hasattr(args, ns_dest):
116                continue
117
118            if not argument.allow_none and getattr(args, ns_dest) is None:
119                continue
120
121            type_adapter = self._get_matching_type_adapter(field)
122            field.value = type_adapter.parse_argument(args, ns_dest, argument, field.value)
123
124    def update_namespace(self, namespace: argparse.Namespace, obj: Any):
125        """
126        Update the argparse namespace with the object's field values.
127
128        Args:
129            namespace (argparse.Namespace): The argparse namespace to be updated.
130            obj (Any): The object containing annotations for command-line arguments.
131        """
132        for name, (field, argument) in self._annotation_finder.find(obj).items():
133            dest = name if argument.dest is None else argument.dest
134            ns_dest = self.to_namespace_str(dest)
135            namespace.__setattr__(ns_dest, field.value)
136
137    def _get_matching_type_adapter(self, field: DataField) -> BaseTypeAdapter:
138        """
139        Get the type adapter that matches the data type of a field.
140
141        Args:
142            field (DataField): The DataField with a specific data type.
143
144        Returns:
145            BaseTypeAdapter: The type adapter that matches the data type, or the default serializer if no match is found.
146        """
147        for type_adapter in self.type_adapters:
148            if type_adapter.handles_type(field.value):
149                return type_adapter
150        return self.default_serializer
151
152    @staticmethod
153    def to_argument_str(name: str) -> str:
154        """
155        Convert a name to a format suitable for command-line arguments (replace underscores with dashes).
156
157        Args:
158            name (str): The original name.
159
160        Returns:
161            str: The name in the format suitable for command-line arguments.
162        """
163        return name.replace("_", "-")
164
165    @staticmethod
166    def to_namespace_str(name: str) -> str:
167        """
168        Convert a command-line argument name to a namespace attribute name (replace dashes with underscores).
169
170        Args:
171            name (str): The command-line argument name.
172
173        Returns:
174            str: The corresponding namespace attribute name.
175        """
176
177        if name.startswith("--"):
178            name = name[2:]
179
180        return name.replace("-", "_")
181
182
183DefaultArguments = Arguments()
class Arguments:
 17class Arguments:
 18    """
 19    A class for handling command-line arguments based on annotations in objects.
 20
 21    Attributes:
 22        type_adapters (List[BaseTypeAdapter]): A list of type adapters to handle specific data types.
 23        default_serializer (BaseTypeAdapter): The default type adapter for serialization.
 24        _annotation_finder (AnnotationFinder): An instance of AnnotationFinder for finding Argument annotations in objects.
 25    """
 26
 27    def __init__(self):
 28        """
 29        Initialize an Arguments instance with default configuration.
 30        """
 31        self.type_adapters: List[BaseTypeAdapter] = [
 32            BooleanTypeAdapter(),
 33            EnumTypeAdapter(),
 34            VectorTypeAdapter(),
 35            PathTypeAdapter()
 36        ]
 37        self.default_serializer: BaseTypeAdapter = DefaultTypeAdapter()
 38
 39        # setup annotation finder
 40        def _is_field_valid(field: DataField, annotation: Argument):
 41            if callable(field.value):
 42                return False
 43            return True
 44
 45        self._annotation_finder: AnnotationFinder[Argument] = AnnotationFinder(Argument, _is_field_valid,
 46                                                                               recursive=True)
 47
 48    def add_and_configure(self,
 49                          parser: argparse.ArgumentParser,
 50                          obj: Any,
 51                          use_attribute_path_as_name: bool = False) -> argparse.Namespace:
 52        """
 53        Add command-line arguments to the parser and configure them based on annotations in the object.
 54
 55        Args:
 56            parser (argparse.ArgumentParser): The argparse parser to which the arguments will be added.
 57            obj (Any): The object containing annotations for command-line arguments.
 58            use_attribute_path_as_name (bool): Use the attribute path as name. This allows nested attributes share the same name.
 59
 60        Returns:
 61            argparse.Namespace: The parsed namespace containing the configured command-line arguments.
 62        """
 63        self.add_arguments(parser, obj, use_attribute_path_as_name)
 64        args = parser.parse_args()
 65        self.configure(args, obj)
 66        return args
 67
 68    def add_arguments(self,
 69                      parser: argparse.ArgumentParser, obj: Any,
 70                      use_attribute_path_as_name: bool = False):
 71        """
 72        Add command-line arguments to the parser based on annotations in the object.
 73
 74        Args:
 75            parser (argparse.ArgumentParser): The argparse parser to which the arguments will be added.
 76            obj (Any): The object containing annotations for command-line arguments.
 77            use_attribute_path_as_name (bool): Use the attribute path as name. This allows nested attributes share the same name.
 78        """
 79        groups = defaultdict(list)
 80
 81        for attribute_identifier, (field, argument) in self._annotation_finder.find_with_identifier(obj).items():
 82            if argument.dest is None:
 83                attribute_name = attribute_identifier.path if use_attribute_path_as_name else attribute_identifier.name
 84                argument.dest = f"--{self.to_argument_str(attribute_name)}"
 85
 86            groups[argument.group].append((field, argument))
 87
 88        group_keys = sorted(groups.keys(), key=lambda x: (x is not None, x))
 89        if None in group_keys:
 90            group_keys.remove(None)
 91            group_keys.insert(0, None)
 92
 93        parser_groups = {g: parser.add_argument_group(g) for g in group_keys if g is not None}
 94        parser_groups[None] = parser
 95
 96        for key in group_keys:
 97            p = parser_groups[key]
 98
 99            for field, argument in groups[key]:
100                type_adapter = self._get_matching_type_adapter(field)
101                type_adapter.add_argument(p, argument, field.value)
102
103    def configure(self, args: argparse.Namespace, obj: Any):
104        """
105        Configure object fields based on the parsed command-line arguments.
106
107        Args:
108            args (argparse.Namespace): The parsed namespace containing the command-line arguments.
109            obj (Any): The object containing annotations for command-line arguments.
110        """
111        for name, (field, argument) in self._annotation_finder.find(obj).items():
112            dest = name if argument.dest is None else argument.dest
113            ns_dest = self.to_namespace_str(dest)
114
115            # only override if CLI actually provided it
116            if not hasattr(args, ns_dest):
117                continue
118
119            if not argument.allow_none and getattr(args, ns_dest) is None:
120                continue
121
122            type_adapter = self._get_matching_type_adapter(field)
123            field.value = type_adapter.parse_argument(args, ns_dest, argument, field.value)
124
125    def update_namespace(self, namespace: argparse.Namespace, obj: Any):
126        """
127        Update the argparse namespace with the object's field values.
128
129        Args:
130            namespace (argparse.Namespace): The argparse namespace to be updated.
131            obj (Any): The object containing annotations for command-line arguments.
132        """
133        for name, (field, argument) in self._annotation_finder.find(obj).items():
134            dest = name if argument.dest is None else argument.dest
135            ns_dest = self.to_namespace_str(dest)
136            namespace.__setattr__(ns_dest, field.value)
137
138    def _get_matching_type_adapter(self, field: DataField) -> BaseTypeAdapter:
139        """
140        Get the type adapter that matches the data type of a field.
141
142        Args:
143            field (DataField): The DataField with a specific data type.
144
145        Returns:
146            BaseTypeAdapter: The type adapter that matches the data type, or the default serializer if no match is found.
147        """
148        for type_adapter in self.type_adapters:
149            if type_adapter.handles_type(field.value):
150                return type_adapter
151        return self.default_serializer
152
153    @staticmethod
154    def to_argument_str(name: str) -> str:
155        """
156        Convert a name to a format suitable for command-line arguments (replace underscores with dashes).
157
158        Args:
159            name (str): The original name.
160
161        Returns:
162            str: The name in the format suitable for command-line arguments.
163        """
164        return name.replace("_", "-")
165
166    @staticmethod
167    def to_namespace_str(name: str) -> str:
168        """
169        Convert a command-line argument name to a namespace attribute name (replace dashes with underscores).
170
171        Args:
172            name (str): The command-line argument name.
173
174        Returns:
175            str: The corresponding namespace attribute name.
176        """
177
178        if name.startswith("--"):
179            name = name[2:]
180
181        return name.replace("-", "_")

A class for handling command-line arguments based on annotations in objects.

Attributes: type_adapters (List[BaseTypeAdapter]): A list of type adapters to handle specific data types. default_serializer (BaseTypeAdapter): The default type adapter for serialization. _annotation_finder (AnnotationFinder): An instance of AnnotationFinder for finding Argument annotations in objects.

Arguments()
27    def __init__(self):
28        """
29        Initialize an Arguments instance with default configuration.
30        """
31        self.type_adapters: List[BaseTypeAdapter] = [
32            BooleanTypeAdapter(),
33            EnumTypeAdapter(),
34            VectorTypeAdapter(),
35            PathTypeAdapter()
36        ]
37        self.default_serializer: BaseTypeAdapter = DefaultTypeAdapter()
38
39        # setup annotation finder
40        def _is_field_valid(field: DataField, annotation: Argument):
41            if callable(field.value):
42                return False
43            return True
44
45        self._annotation_finder: AnnotationFinder[Argument] = AnnotationFinder(Argument, _is_field_valid,
46                                                                               recursive=True)

Initialize an Arguments instance with default configuration.

type_adapters: List[BaseTypeAdapter]
default_serializer: BaseTypeAdapter
def add_and_configure( self, parser: argparse.ArgumentParser, obj: Any, use_attribute_path_as_name: bool = False) -> argparse.Namespace:
48    def add_and_configure(self,
49                          parser: argparse.ArgumentParser,
50                          obj: Any,
51                          use_attribute_path_as_name: bool = False) -> argparse.Namespace:
52        """
53        Add command-line arguments to the parser and configure them based on annotations in the object.
54
55        Args:
56            parser (argparse.ArgumentParser): The argparse parser to which the arguments will be added.
57            obj (Any): The object containing annotations for command-line arguments.
58            use_attribute_path_as_name (bool): Use the attribute path as name. This allows nested attributes share the same name.
59
60        Returns:
61            argparse.Namespace: The parsed namespace containing the configured command-line arguments.
62        """
63        self.add_arguments(parser, obj, use_attribute_path_as_name)
64        args = parser.parse_args()
65        self.configure(args, obj)
66        return args

Add command-line arguments to the parser and configure them based on annotations in the object.

Args: parser (argparse.ArgumentParser): The argparse parser to which the arguments will be added. obj (Any): The object containing annotations for command-line arguments. use_attribute_path_as_name (bool): Use the attribute path as name. This allows nested attributes share the same name.

Returns: argparse.Namespace: The parsed namespace containing the configured command-line arguments.

def add_arguments( self, parser: argparse.ArgumentParser, obj: Any, use_attribute_path_as_name: bool = False):
 68    def add_arguments(self,
 69                      parser: argparse.ArgumentParser, obj: Any,
 70                      use_attribute_path_as_name: bool = False):
 71        """
 72        Add command-line arguments to the parser based on annotations in the object.
 73
 74        Args:
 75            parser (argparse.ArgumentParser): The argparse parser to which the arguments will be added.
 76            obj (Any): The object containing annotations for command-line arguments.
 77            use_attribute_path_as_name (bool): Use the attribute path as name. This allows nested attributes share the same name.
 78        """
 79        groups = defaultdict(list)
 80
 81        for attribute_identifier, (field, argument) in self._annotation_finder.find_with_identifier(obj).items():
 82            if argument.dest is None:
 83                attribute_name = attribute_identifier.path if use_attribute_path_as_name else attribute_identifier.name
 84                argument.dest = f"--{self.to_argument_str(attribute_name)}"
 85
 86            groups[argument.group].append((field, argument))
 87
 88        group_keys = sorted(groups.keys(), key=lambda x: (x is not None, x))
 89        if None in group_keys:
 90            group_keys.remove(None)
 91            group_keys.insert(0, None)
 92
 93        parser_groups = {g: parser.add_argument_group(g) for g in group_keys if g is not None}
 94        parser_groups[None] = parser
 95
 96        for key in group_keys:
 97            p = parser_groups[key]
 98
 99            for field, argument in groups[key]:
100                type_adapter = self._get_matching_type_adapter(field)
101                type_adapter.add_argument(p, argument, field.value)

Add command-line arguments to the parser based on annotations in the object.

Args: parser (argparse.ArgumentParser): The argparse parser to which the arguments will be added. obj (Any): The object containing annotations for command-line arguments. use_attribute_path_as_name (bool): Use the attribute path as name. This allows nested attributes share the same name.

def configure(self, args: argparse.Namespace, obj: Any):
103    def configure(self, args: argparse.Namespace, obj: Any):
104        """
105        Configure object fields based on the parsed command-line arguments.
106
107        Args:
108            args (argparse.Namespace): The parsed namespace containing the command-line arguments.
109            obj (Any): The object containing annotations for command-line arguments.
110        """
111        for name, (field, argument) in self._annotation_finder.find(obj).items():
112            dest = name if argument.dest is None else argument.dest
113            ns_dest = self.to_namespace_str(dest)
114
115            # only override if CLI actually provided it
116            if not hasattr(args, ns_dest):
117                continue
118
119            if not argument.allow_none and getattr(args, ns_dest) is None:
120                continue
121
122            type_adapter = self._get_matching_type_adapter(field)
123            field.value = type_adapter.parse_argument(args, ns_dest, argument, field.value)

Configure object fields based on the parsed command-line arguments.

Args: args (argparse.Namespace): The parsed namespace containing the command-line arguments. obj (Any): The object containing annotations for command-line arguments.

def update_namespace(self, namespace: argparse.Namespace, obj: Any):
125    def update_namespace(self, namespace: argparse.Namespace, obj: Any):
126        """
127        Update the argparse namespace with the object's field values.
128
129        Args:
130            namespace (argparse.Namespace): The argparse namespace to be updated.
131            obj (Any): The object containing annotations for command-line arguments.
132        """
133        for name, (field, argument) in self._annotation_finder.find(obj).items():
134            dest = name if argument.dest is None else argument.dest
135            ns_dest = self.to_namespace_str(dest)
136            namespace.__setattr__(ns_dest, field.value)

Update the argparse namespace with the object's field values.

Args: namespace (argparse.Namespace): The argparse namespace to be updated. obj (Any): The object containing annotations for command-line arguments.

@staticmethod
def to_argument_str(name: str) -> str:
153    @staticmethod
154    def to_argument_str(name: str) -> str:
155        """
156        Convert a name to a format suitable for command-line arguments (replace underscores with dashes).
157
158        Args:
159            name (str): The original name.
160
161        Returns:
162            str: The name in the format suitable for command-line arguments.
163        """
164        return name.replace("_", "-")

Convert a name to a format suitable for command-line arguments (replace underscores with dashes).

Args: name (str): The original name.

Returns: str: The name in the format suitable for command-line arguments.

@staticmethod
def to_namespace_str(name: str) -> str:
166    @staticmethod
167    def to_namespace_str(name: str) -> str:
168        """
169        Convert a command-line argument name to a namespace attribute name (replace dashes with underscores).
170
171        Args:
172            name (str): The command-line argument name.
173
174        Returns:
175            str: The corresponding namespace attribute name.
176        """
177
178        if name.startswith("--"):
179            name = name[2:]
180
181        return name.replace("-", "_")

Convert a command-line argument name to a namespace attribute name (replace dashes with underscores).

Args: name (str): The command-line argument name.

Returns: str: The corresponding namespace attribute name.

DefaultArguments = <Arguments object>