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)
REF_DELIM: str = ':'
class Reference(edq.util.json.DictConverter):
 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,

Reference( raw_input: Union[str, Reference], check_alias: bool = True)
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.

file_path: str | None

The file_path component of the reflection reference (or None).

module_name: str | None

The module_name component of the reflection reference (or None).

short_name: str

The short_name component of the reflection reference (or None).

@staticmethod
def build_string(short_name: str, file_path: str | None, module_name: str | None) -> str:
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.

@staticmethod
def parse_string( text: str, check_alias: bool = True) -> tuple[str | None, str | None, str]:
 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.

def to_dict(self) -> dict[str, typing.Any]:
114    def to_dict(self) -> dict[str, typing.Any]:
115        return vars(self).copy()

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.

@classmethod
def from_dict(cls, data: dict[str, typing.Any]) -> Any:
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.

def fetch(reference: Reference | str) -> Any:
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.

def new_object( reference: Reference | str, *args: Any, **kwargs: Any) -> Any:
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.

def resolve_and_fetch( cls: Type, raw_object: Union[~T, Reference, str]) -> Any:
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.

def get_qualified_name(target: type | object | Reference | str) -> str:
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.