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()
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.
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.
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.
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.
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.
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.
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.
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.