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