Repository: rhayes777/PyAutoFit
Pull request #1036 (2024-08-12 09:10:36)

feature/astropy magic
Various fixes in autofit Model creation to ensure astropy is supported



Patch:
diff --git a/autofit/mapper/mock/mock_model.py b/autofit/mapper/mock/mock_model.py
index fe8e299ba..f5811973b 100644
--- a/autofit/mapper/mock/mock_model.py
+++ b/autofit/mapper/mock/mock_model.py
@@ -41,7 +41,7 @@ class MockClassx3(MockClassx4):
 class MockClassx2Tuple:
     def __init__(self, one_tuple=(0.0, 0.0)):
         """Abstract MockParent, describing an object with y, x cartesian
-        coordinates """
+        coordinates"""
         self.one_tuple = one_tuple
 
     def __eq__(self, other):
@@ -94,7 +94,6 @@ class MockWithTuple:
 
 
 class MockOverload:
-
     def __init__(self, one=1.0):
         self.one = one
 
@@ -112,11 +111,11 @@ class MockOverload:
 
 class MockComponents:
     def __init__(
-            self,
-            components_0: list = None,
-            components_1: list = None,
-            parameter=None,
-            **kwargs
+        self,
+        components_0: list = None,
+        components_1: list = None,
+        parameter=None,
+        **kwargs
     ):
         self.parameter = parameter
         self.group_0 = components_0
@@ -134,7 +133,7 @@ class MockParent:
 
 class MockChildTuple(MockParent):
     def __init__(self, tup=(0.0, 0.0)):
-        """ Generic circular profiles class to contain functions shared by light and
+        """Generic circular profiles class to contain functions shared by light and
         mass profiles.
 
         Parameters
@@ -147,7 +146,7 @@ class MockChildTuple(MockParent):
 
 class MockChildTuplex2(MockChildTuple):
     def __init__(self, tup=(0.0, 0.0), one=1.0, two=0.0):
-        """ Generic elliptical profiles class to contain functions shared by light
+        """Generic elliptical profiles class to contain functions shared by light
         and mass profiles.
 
         Parameters
@@ -166,7 +165,7 @@ class MockChildTuplex2(MockChildTuple):
 
 class MockChildTuplex3(MockChildTuple):
     def __init__(self, tup=(0.0, 0.0), one=1.0, two=0.0, three=0.0):
-        """ Generic elliptical profiles class to contain functions shared by light
+        """Generic elliptical profiles class to contain functions shared by light
         and mass profiles.
 
         Parameters
@@ -182,3 +181,13 @@ class MockChildTuplex3(MockChildTuple):
         self.one = one
         self.two = two
         self.three = three
+
+
+class Parameter:
+    def __init__(self, value: float = 0.5):
+        self.value = value
+
+
+class WithString:
+    def __init__(self, arg: "Parameter"):
+        self.arg = arg
diff --git a/autofit/mapper/prior_model/prior_model.py b/autofit/mapper/prior_model/prior_model.py
index 43f2dcc8c..7f43c145f 100644
--- a/autofit/mapper/prior_model/prior_model.py
+++ b/autofit/mapper/prior_model/prior_model.py
@@ -1,10 +1,9 @@
-import builtins
 import collections.abc
 import copy
 import inspect
 import logging
-from typing import List
 import typing
+from typing import *
 
 from autofit.jax_wrapper import register_pytree_node_class, register_pytree_node
 
@@ -16,6 +15,7 @@ from autofit.mapper.prior.abstract import Prior
 from autofit.mapper.prior.deferred import DeferredInstance
 from autofit.mapper.prior.tuple_prior import TuplePrior
 from autofit.mapper.prior_model.abstract import AbstractPriorModel
+from autofit.mapper.prior_model.util import gather_namespaces
 from autofit.tools.namer import namer
 
 logger = logging.getLogger(__name__)
@@ -112,19 +112,23 @@ class Model(AbstractPriorModel):
 
         self.cls = cls
 
-        try:
-            annotations = inspect.getfullargspec(cls).annotations
-            for key, value in annotations.items():
-                if isinstance(value, str):
-                    annotations[key] = getattr(builtins, value)
-        except TypeError:
-            annotations = dict()
+        namespaces = gather_namespaces(cls)
+
+        annotations = typing.get_type_hints(
+            cls.__init__,
+            namespaces,
+            namespaces,
+        )
 
         try:
             arg_spec = inspect.getfullargspec(cls)
             defaults = dict(
                 zip(arg_spec.args[-len(arg_spec.defaults) :], arg_spec.defaults)
             )
+            defaults = {
+                key: value.default if hasattr(value, "default") else value
+                for key, value in defaults.items()
+            }
         except TypeError:
             defaults = {}
 
@@ -170,12 +174,21 @@ class Model(AbstractPriorModel):
                 else:
                     annotation = annotations[arg]
 
-                    if (
-                        hasattr(annotation, "__origin__")
-                        and issubclass(
-                            annotation.__origin__, collections.abc.Collection
+                    if isinstance(annotation, str):
+                        continue
+
+                    if arg in defaults:
+                        value = self._convert_value(defaults[arg])
+                    elif (
+                        (
+                            hasattr(annotation, "__origin__")
+                            and issubclass(
+                                annotation.__origin__, collections.abc.Collection
+                            )
                         )
-                    ) or isinstance(annotation, collections.abc.Collection):
+                        or isinstance(annotation, collections.abc.Collection)
+                        or issubclass(annotation, collections.abc.Collection)
+                    ):
                         from autofit.mapper.prior_model.collection import Collection
 
                         value = Collection()
@@ -279,7 +292,11 @@ class Model(AbstractPriorModel):
         """
         if self.cls not in class_args_dict:
             try:
-                class_args_dict[self.cls] = inspect.getfullargspec(self.cls).args[1:]
+                class_args_dict[self.cls] = [
+                    arg
+                    for arg in inspect.getfullargspec(self.cls).args
+                    if arg != "self"
+                ]
             except TypeError:
                 class_args_dict[self.cls] = []
         return class_args_dict[self.cls]
diff --git a/autofit/mapper/prior_model/util.py b/autofit/mapper/prior_model/util.py
index b364ce9bf..822ce5035 100644
--- a/autofit/mapper/prior_model/util.py
+++ b/autofit/mapper/prior_model/util.py
@@ -1,3 +1,7 @@
+import inspect
+from typing import Type, Dict
+import typing
+
 from autofit.mapper.prior_model.attribute_pair import AttributeNameValue
 
 
@@ -5,3 +9,25 @@ class PriorModelNameValue(AttributeNameValue):
     @property
     def prior_model(self):
         return self.value
+
+
+def gather_namespaces(cls: Type) -> Dict[str, Dict]:
+    """
+    Recursively gather the globals and locals for a given class and its parent classes.
+    """
+    namespaces = {}
+
+    try:
+        for base in inspect.getmro(cls):
+            if base is object:
+                continue
+
+            module = inspect.getmodule(base)
+            if module:
+                namespaces.update(vars(module))
+    except AttributeError:
+        pass
+
+    namespaces.update(vars(typing))
+
+    return namespaces


Test patch:
diff --git a/test_autofit/config/priors/mock_model.yaml b/test_autofit/config/priors/mock_model.yaml
index 74cbc268a..3c21fcfbf 100644
--- a/test_autofit/config/priors/mock_model.yaml
+++ b/test_autofit/config/priors/mock_model.yaml
@@ -1,3 +1,14 @@
+Parameter:
+  value:
+    gaussian_limits:
+      lower: 0.0
+      upper: 1.0
+    lower_limit: 0.0
+    type: Uniform
+    upper_limit: 1.0
+    width_modifier:
+      type: Absolute
+      value: 0.2
 MockChildTuple:
   tup_0:
     gaussian_limits:
diff --git a/test_autofit/mapper/model/test_regression.py b/test_autofit/mapper/model/test_regression.py
index 1955cc30b..fa381fb6a 100644
--- a/test_autofit/mapper/model/test_regression.py
+++ b/test_autofit/mapper/model/test_regression.py
@@ -5,6 +5,7 @@ import pytest
 import autofit as af
 from autoconf.exc import ConfigException
 from autofit.example.model import PhysicalNFW
+from autofit.mapper.mock.mock_model import WithString
 from autofit.mapper.model_object import Identifier
 
 
@@ -158,7 +159,6 @@ def test_random_instance(model_with_assertion):
 
 
 class TestModel:
-
     __test__ = False
 
     def __init__(self, items: List[float]):
@@ -173,3 +173,8 @@ def test_typing_annotations():
 def test_no_default_tuple_priors():
     model = af.Model(PhysicalNFW)
     assert model.prior_count == 6
+
+
+def test_string_annotation():
+    model = af.Model(WithString)
+    assert model.instance_from_prior_medians().arg.value == 0.5