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            if not argument.allow_none and getattr(args, ns_dest) is None:
115                continue
116
117            type_adapter = self._get_matching_type_adapter(field)
118            field.value = type_adapter.parse_argument(args, ns_dest, argument, field.value)
119
120    def update_namespace(self, namespace: argparse.Namespace, obj: Any):
121        """
122        Update the argparse namespace with the object's field values.
123
124        Args:
125            namespace (argparse.Namespace): The argparse namespace to be updated.
126            obj (Any): The object containing annotations for command-line arguments.
127        """
128        for name, (field, argument) in self._annotation_finder.find(obj).items():
129            dest = name if argument.dest is None else argument.dest
130            ns_dest = self.to_namespace_str(dest)
131            namespace.__setattr__(ns_dest, field.value)
132
133    def _get_matching_type_adapter(self, field: DataField) -> BaseTypeAdapter:
134        """
135        Get the type adapter that matches the data type of a field.
136
137        Args:
138            field (DataField): The DataField with a specific data type.
139
140        Returns:
141            BaseTypeAdapter: The type adapter that matches the data type, or the default serializer if no match is found.
142        """
143        for type_adapter in self.type_adapters:
144            if type_adapter.handles_type(field.value):
145                return type_adapter
146        return self.default_serializer
147
148    @staticmethod
149    def to_argument_str(name: str) -> str:
150        """
151        Convert a name to a format suitable for command-line arguments (replace underscores with dashes).
152
153        Args:
154            name (str): The original name.
155
156        Returns:
157            str: The name in the format suitable for command-line arguments.
158        """
159        return name.replace("_", "-")
160
161    @staticmethod
162    def to_namespace_str(name: str) -> str:
163        """
164        Convert a command-line argument name to a namespace attribute name (replace dashes with underscores).
165
166        Args:
167            name (str): The command-line argument name.
168
169        Returns:
170            str: The corresponding namespace attribute name.
171        """
172
173        if name.startswith("--"):
174            name = name[2:]
175
176        return name.replace("-", "_")
177
178
179DefaultArguments = 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            if not argument.allow_none and getattr(args, ns_dest) is None:
116                continue
117
118            type_adapter = self._get_matching_type_adapter(field)
119            field.value = type_adapter.parse_argument(args, ns_dest, argument, field.value)
120
121    def update_namespace(self, namespace: argparse.Namespace, obj: Any):
122        """
123        Update the argparse namespace with the object's field values.
124
125        Args:
126            namespace (argparse.Namespace): The argparse namespace to be updated.
127            obj (Any): The object containing annotations for command-line arguments.
128        """
129        for name, (field, argument) in self._annotation_finder.find(obj).items():
130            dest = name if argument.dest is None else argument.dest
131            ns_dest = self.to_namespace_str(dest)
132            namespace.__setattr__(ns_dest, field.value)
133
134    def _get_matching_type_adapter(self, field: DataField) -> BaseTypeAdapter:
135        """
136        Get the type adapter that matches the data type of a field.
137
138        Args:
139            field (DataField): The DataField with a specific data type.
140
141        Returns:
142            BaseTypeAdapter: The type adapter that matches the data type, or the default serializer if no match is found.
143        """
144        for type_adapter in self.type_adapters:
145            if type_adapter.handles_type(field.value):
146                return type_adapter
147        return self.default_serializer
148
149    @staticmethod
150    def to_argument_str(name: str) -> str:
151        """
152        Convert a name to a format suitable for command-line arguments (replace underscores with dashes).
153
154        Args:
155            name (str): The original name.
156
157        Returns:
158            str: The name in the format suitable for command-line arguments.
159        """
160        return name.replace("_", "-")
161
162    @staticmethod
163    def to_namespace_str(name: str) -> str:
164        """
165        Convert a command-line argument name to a namespace attribute name (replace dashes with underscores).
166
167        Args:
168            name (str): The command-line argument name.
169
170        Returns:
171            str: The corresponding namespace attribute name.
172        """
173
174        if name.startswith("--"):
175            name = name[2:]
176
177        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            if not argument.allow_none and getattr(args, ns_dest) is None:
116                continue
117
118            type_adapter = self._get_matching_type_adapter(field)
119            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):
121    def update_namespace(self, namespace: argparse.Namespace, obj: Any):
122        """
123        Update the argparse namespace with the object's field values.
124
125        Args:
126            namespace (argparse.Namespace): The argparse namespace to be updated.
127            obj (Any): The object containing annotations for command-line arguments.
128        """
129        for name, (field, argument) in self._annotation_finder.find(obj).items():
130            dest = name if argument.dest is None else argument.dest
131            ns_dest = self.to_namespace_str(dest)
132            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:
149    @staticmethod
150    def to_argument_str(name: str) -> str:
151        """
152        Convert a name to a format suitable for command-line arguments (replace underscores with dashes).
153
154        Args:
155            name (str): The original name.
156
157        Returns:
158            str: The name in the format suitable for command-line arguments.
159        """
160        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:
162    @staticmethod
163    def to_namespace_str(name: str) -> str:
164        """
165        Convert a command-line argument name to a namespace attribute name (replace dashes with underscores).
166
167        Args:
168            name (str): The command-line argument name.
169
170        Returns:
171            str: The corresponding namespace attribute name.
172        """
173
174        if name.startswith("--"):
175            name = name[2:]
176
177        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>