pacai.util.reflection
Reflection is the ability for a program to examine and modify its own code and structure when running. For example, you may want to find the type of a variable or create an object without knowing its type when you write the code. See: https://en.wikipedia.org/wiki/Reflective_programming .
This file aims to contain all the reflection necessary for this project (as it can be confusing for students).
1""" 2Reflection is the ability for a program to examine and modify its own code and structure when running. 3For example, you may want to find the type of a variable or create an object without knowing its type when you write the code. 4See: https://en.wikipedia.org/wiki/Reflective_programming . 5 6This file aims to contain all the reflection necessary for this project 7(as it can be confusing for students). 8""" 9 10import typing 11 12import edq.util.json 13import edq.util.pyimport 14import edq.util.reflection 15 16import pacai.util.alias 17 18REF_DELIM: str = ':' 19 20class Reference(edq.util.json.DictConverter): 21 """ 22 A Reference is constructed from a formatted that references a specific Python definition (e.g. class or function). 23 The rough basic structure of a reference is: `[<path>:][<qualified package name>.][<module name>.]<short name>`. 24 This means that a valid reference for this class can either: 25 1) `pacai.util.reflection.Reference` -- a fully qualified name. 26 2) `pacai/util/reflection.py:Reference` -- a path and class name, 27 """ 28 29 def __init__(self, 30 raw_input: typing.Union[str, 'Reference'], 31 check_alias: bool = True, 32 ) -> None: 33 """ Construct and validate a reference. """ 34 35 if (isinstance(raw_input, Reference)): 36 file_path = raw_input.file_path 37 module_name = raw_input.module_name 38 short_name = raw_input.short_name 39 else: 40 file_path, module_name, short_name = Reference.parse_string(raw_input, check_alias) 41 42 self.file_path: str | None = file_path 43 """ The file_path component of the reflection reference (or None). """ 44 45 self.module_name: str | None = module_name 46 """ The module_name component of the reflection reference (or None). """ 47 48 self.short_name: str = short_name 49 """ The short_name component of the reflection reference (or None). """ 50 51 def __str__(self) -> str: 52 return Reference.build_string(self.short_name, self.file_path, self.module_name) 53 54 def __repr__(self) -> str: 55 return str(self) 56 57 @staticmethod 58 def build_string(short_name: str, file_path: str | None, module_name: str | None) -> str: 59 """ 60 Build a string representation from the given components. 61 The output should be able to be used as an argument to construct a Reference. 62 """ 63 64 text = short_name 65 66 if (module_name is not None): 67 text = module_name + '.' + text 68 69 if (file_path is not None): 70 text = file_path + REF_DELIM + text 71 72 return text 73 74 @staticmethod 75 def parse_string(text: str, check_alias: bool = True) -> tuple[str | None, str | None, str]: 76 """ Parse out the key reference components from a string. """ 77 78 text = text.strip() 79 if (len(text) == 0): 80 raise ValueError("Cannot parse a reflection reference from an empty string.") 81 82 # Check if this looks like an alias. 83 if (check_alias and ('.' not in text)): 84 text = pacai.util.alias.lookup(text, text) 85 86 parts = text.rsplit(REF_DELIM, 1) 87 88 file_path = None 89 remaining = parts[-1].strip() 90 91 if (len(parts) > 1): 92 file_path = parts[0].strip() 93 94 if (len(remaining) == 0): 95 raise ValueError("Cannot parse a reflection reference without a short name.") 96 97 parts = remaining.split('.') 98 99 module_name = None 100 short_name = parts[-1].strip() 101 102 if (len(parts) > 1): 103 module_name = '.'.join(parts[0:-1]).strip() 104 105 if ((file_path is not None) and (module_name is not None)): 106 raise ValueError(f"Cannot specify both a file path and module name for reflection reference: '{text}'.") 107 108 if ((file_path is None) and (module_name is None)): 109 raise ValueError(f"Cannot specify a (non-alias) short name alone, need a file_path or module name for reflection reference: '{text}'.") 110 111 return file_path, module_name, short_name 112 113 def to_dict(self) -> dict[str, typing.Any]: 114 return vars(self).copy() 115 116 @classmethod 117 def from_dict(cls, data: dict[str, typing.Any]) -> typing.Any: 118 text = Reference.build_string(data.get('short_name', ''), data.get('file_path', None), data.get('module_name', None)) 119 return cls(text) 120 121def fetch(reference: Reference | str) -> typing.Any: 122 """ Fetch the target of the reference. """ 123 124 if (isinstance(reference, str)): 125 reference = Reference(reference) 126 127 module = _import_module(reference) 128 129 target = getattr(module, reference.short_name, None) 130 if (target is None): 131 raise ValueError(f"Cannot find target '{reference.short_name}' in reflection reference '{reference}'.") 132 133 return target 134 135def new_object(reference: Reference | str, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: 136 """ 137 Create a new instance of the specified class, 138 passing along the args and kwargs. 139 """ 140 141 target_class = fetch(reference) 142 return target_class(*args, **kwargs) 143 144T = typing.TypeVar('T') 145 146def resolve_and_fetch( 147 cls: typing.Type, 148 raw_object: T | Reference | str, 149 ) -> typing.Any: 150 """ 151 Resolve the given raw object into the specified class. 152 If it is already an object of that type, just return it. 153 If it is a reference or string, resolve the reference and fetch the reference. 154 """ 155 156 if (isinstance(raw_object, cls)): 157 return raw_object 158 159 reference = Reference(typing.cast(Reference | str, raw_object)) 160 result = fetch(reference) 161 162 if (not isinstance(result, cls)): 163 raise ValueError(f"Target '{reference}' is not of type '{cls}', found type '{type(result)}'.") 164 165 return result 166 167def _import_module(reference: Reference) -> typing.Any: 168 """ 169 Import and return the module for the given reflection reference. 170 This may involve importing files. 171 """ 172 173 # Load from a path. 174 if (reference.file_path is not None): 175 return edq.util.pyimport.import_path(reference.file_path) 176 177 # Load from a name. 178 if (reference.module_name is not None): 179 return edq.util.pyimport.import_name(reference.module_name) 180 181 raise ValueError(f"Reference does not contain enough information to be imported as a module: '{reference}'.") 182 183def get_qualified_name(target: type | object | Reference | str) -> str: 184 """ 185 Try to get a qualified name for a type (or for the type of an object). 186 Names will not always come out clean. 187 """ 188 189 # If this is a string or reference, just resolve the reference. 190 if (isinstance(target, (Reference, str))): 191 return str(Reference(target)) 192 193 return edq.util.reflection.get_qualified_name(target)
21class Reference(edq.util.json.DictConverter): 22 """ 23 A Reference is constructed from a formatted that references a specific Python definition (e.g. class or function). 24 The rough basic structure of a reference is: `[<path>:][<qualified package name>.][<module name>.]<short name>`. 25 This means that a valid reference for this class can either: 26 1) `pacai.util.reflection.Reference` -- a fully qualified name. 27 2) `pacai/util/reflection.py:Reference` -- a path and class name, 28 """ 29 30 def __init__(self, 31 raw_input: typing.Union[str, 'Reference'], 32 check_alias: bool = True, 33 ) -> None: 34 """ Construct and validate a reference. """ 35 36 if (isinstance(raw_input, Reference)): 37 file_path = raw_input.file_path 38 module_name = raw_input.module_name 39 short_name = raw_input.short_name 40 else: 41 file_path, module_name, short_name = Reference.parse_string(raw_input, check_alias) 42 43 self.file_path: str | None = file_path 44 """ The file_path component of the reflection reference (or None). """ 45 46 self.module_name: str | None = module_name 47 """ The module_name component of the reflection reference (or None). """ 48 49 self.short_name: str = short_name 50 """ The short_name component of the reflection reference (or None). """ 51 52 def __str__(self) -> str: 53 return Reference.build_string(self.short_name, self.file_path, self.module_name) 54 55 def __repr__(self) -> str: 56 return str(self) 57 58 @staticmethod 59 def build_string(short_name: str, file_path: str | None, module_name: str | None) -> str: 60 """ 61 Build a string representation from the given components. 62 The output should be able to be used as an argument to construct a Reference. 63 """ 64 65 text = short_name 66 67 if (module_name is not None): 68 text = module_name + '.' + text 69 70 if (file_path is not None): 71 text = file_path + REF_DELIM + text 72 73 return text 74 75 @staticmethod 76 def parse_string(text: str, check_alias: bool = True) -> tuple[str | None, str | None, str]: 77 """ Parse out the key reference components from a string. """ 78 79 text = text.strip() 80 if (len(text) == 0): 81 raise ValueError("Cannot parse a reflection reference from an empty string.") 82 83 # Check if this looks like an alias. 84 if (check_alias and ('.' not in text)): 85 text = pacai.util.alias.lookup(text, text) 86 87 parts = text.rsplit(REF_DELIM, 1) 88 89 file_path = None 90 remaining = parts[-1].strip() 91 92 if (len(parts) > 1): 93 file_path = parts[0].strip() 94 95 if (len(remaining) == 0): 96 raise ValueError("Cannot parse a reflection reference without a short name.") 97 98 parts = remaining.split('.') 99 100 module_name = None 101 short_name = parts[-1].strip() 102 103 if (len(parts) > 1): 104 module_name = '.'.join(parts[0:-1]).strip() 105 106 if ((file_path is not None) and (module_name is not None)): 107 raise ValueError(f"Cannot specify both a file path and module name for reflection reference: '{text}'.") 108 109 if ((file_path is None) and (module_name is None)): 110 raise ValueError(f"Cannot specify a (non-alias) short name alone, need a file_path or module name for reflection reference: '{text}'.") 111 112 return file_path, module_name, short_name 113 114 def to_dict(self) -> dict[str, typing.Any]: 115 return vars(self).copy() 116 117 @classmethod 118 def from_dict(cls, data: dict[str, typing.Any]) -> typing.Any: 119 text = Reference.build_string(data.get('short_name', ''), data.get('file_path', None), data.get('module_name', None)) 120 return cls(text)
A Reference is constructed from a formatted that references a specific Python definition (e.g. class or function).
The rough basic structure of a reference is: [<path>:][<qualified package name>.][<module name>.]<short name>.
This means that a valid reference for this class can either:
1) pacai.util.reflection.Reference -- a fully qualified name.
2) pacai/util/reflection.py:Reference -- a path and class name,
30 def __init__(self, 31 raw_input: typing.Union[str, 'Reference'], 32 check_alias: bool = True, 33 ) -> None: 34 """ Construct and validate a reference. """ 35 36 if (isinstance(raw_input, Reference)): 37 file_path = raw_input.file_path 38 module_name = raw_input.module_name 39 short_name = raw_input.short_name 40 else: 41 file_path, module_name, short_name = Reference.parse_string(raw_input, check_alias) 42 43 self.file_path: str | None = file_path 44 """ The file_path component of the reflection reference (or None). """ 45 46 self.module_name: str | None = module_name 47 """ The module_name component of the reflection reference (or None). """ 48 49 self.short_name: str = short_name 50 """ The short_name component of the reflection reference (or None). """
Construct and validate a reference.
58 @staticmethod 59 def build_string(short_name: str, file_path: str | None, module_name: str | None) -> str: 60 """ 61 Build a string representation from the given components. 62 The output should be able to be used as an argument to construct a Reference. 63 """ 64 65 text = short_name 66 67 if (module_name is not None): 68 text = module_name + '.' + text 69 70 if (file_path is not None): 71 text = file_path + REF_DELIM + text 72 73 return text
Build a string representation from the given components. The output should be able to be used as an argument to construct a Reference.
75 @staticmethod 76 def parse_string(text: str, check_alias: bool = True) -> tuple[str | None, str | None, str]: 77 """ Parse out the key reference components from a string. """ 78 79 text = text.strip() 80 if (len(text) == 0): 81 raise ValueError("Cannot parse a reflection reference from an empty string.") 82 83 # Check if this looks like an alias. 84 if (check_alias and ('.' not in text)): 85 text = pacai.util.alias.lookup(text, text) 86 87 parts = text.rsplit(REF_DELIM, 1) 88 89 file_path = None 90 remaining = parts[-1].strip() 91 92 if (len(parts) > 1): 93 file_path = parts[0].strip() 94 95 if (len(remaining) == 0): 96 raise ValueError("Cannot parse a reflection reference without a short name.") 97 98 parts = remaining.split('.') 99 100 module_name = None 101 short_name = parts[-1].strip() 102 103 if (len(parts) > 1): 104 module_name = '.'.join(parts[0:-1]).strip() 105 106 if ((file_path is not None) and (module_name is not None)): 107 raise ValueError(f"Cannot specify both a file path and module name for reflection reference: '{text}'.") 108 109 if ((file_path is None) and (module_name is None)): 110 raise ValueError(f"Cannot specify a (non-alias) short name alone, need a file_path or module name for reflection reference: '{text}'.") 111 112 return file_path, module_name, short_name
Parse out the key reference components from a string.
Return a dict that can be used to represent this object. If the dict is passed to from_dict(), an identical object should be reconstructed.
A general (but inefficient) implementation is provided by default.
117 @classmethod 118 def from_dict(cls, data: dict[str, typing.Any]) -> typing.Any: 119 text = Reference.build_string(data.get('short_name', ''), data.get('file_path', None), data.get('module_name', None)) 120 return cls(text)
Return an instance of this subclass created using the given dict. If the dict came from to_dict(), the returned object should be identical to the original.
A general (but inefficient) implementation is provided by default.
122def fetch(reference: Reference | str) -> typing.Any: 123 """ Fetch the target of the reference. """ 124 125 if (isinstance(reference, str)): 126 reference = Reference(reference) 127 128 module = _import_module(reference) 129 130 target = getattr(module, reference.short_name, None) 131 if (target is None): 132 raise ValueError(f"Cannot find target '{reference.short_name}' in reflection reference '{reference}'.") 133 134 return target
Fetch the target of the reference.
136def new_object(reference: Reference | str, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: 137 """ 138 Create a new instance of the specified class, 139 passing along the args and kwargs. 140 """ 141 142 target_class = fetch(reference) 143 return target_class(*args, **kwargs)
Create a new instance of the specified class, passing along the args and kwargs.
147def resolve_and_fetch( 148 cls: typing.Type, 149 raw_object: T | Reference | str, 150 ) -> typing.Any: 151 """ 152 Resolve the given raw object into the specified class. 153 If it is already an object of that type, just return it. 154 If it is a reference or string, resolve the reference and fetch the reference. 155 """ 156 157 if (isinstance(raw_object, cls)): 158 return raw_object 159 160 reference = Reference(typing.cast(Reference | str, raw_object)) 161 result = fetch(reference) 162 163 if (not isinstance(result, cls)): 164 raise ValueError(f"Target '{reference}' is not of type '{cls}', found type '{type(result)}'.") 165 166 return result
Resolve the given raw object into the specified class. If it is already an object of that type, just return it. If it is a reference or string, resolve the reference and fetch the reference.
184def get_qualified_name(target: type | object | Reference | str) -> str: 185 """ 186 Try to get a qualified name for a type (or for the type of an object). 187 Names will not always come out clean. 188 """ 189 190 # If this is a string or reference, just resolve the reference. 191 if (isinstance(target, (Reference, str))): 192 return str(Reference(target)) 193 194 return edq.util.reflection.get_qualified_name(target)
Try to get a qualified name for a type (or for the type of an object). Names will not always come out clean.