Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions gin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,17 @@ def _decorate_fn_or_cls(decorator, fn_or_cls, subclass=False):
construction_fn = _find_class_construction_fn(fn_or_cls)

if subclass:

klass = fn_or_cls
while hasattr(klass, '_gin_decorated') and klass._gin_decorated:
klass = klass.__bases__[0]
class DecoratedClass(fn_or_cls):
__doc__ = fn_or_cls.__doc__
__module__ = fn_or_cls.__module__

__class__ = klass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could remove the "_gin_decorated" attribute and associated base class traversal above if we use __class__ = fn_or_cls.__class__ here? I think that would propagate the right __class__ down even when decorated multiple times (although haven't tested).

@gmrukwa gmrukwa Nov 17, 2020

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested that (or at least what I think you think of) and it didn't work so smoothly for external configurables (the case when you have scopes, etc.) 🙂

def __reduce__(self):
return super(DecoratedClass, self).__reduce__()
DecoratedClass.__name__ = fn_or_cls.__name__
DecoratedClass._gin_decorated = True
DecoratedClass.__qualname__ = fn_or_cls.__qualname__
cls = DecoratedClass
else:
Expand Down
1 change: 0 additions & 1 deletion gin/resource_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

"""Module for reading gin configs in the python system path."""
from __future__ import absolute_import
from __future__ import google_type_annotations
from __future__ import print_function

import importlib
Expand Down
305 changes: 305 additions & 0 deletions tests/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io
import logging
import os
import pickle
import threading

from absl.testing import absltest
Expand Down Expand Up @@ -241,6 +242,65 @@ def __init__(self, kwarg1=None, kwarg2=None):
self.kwarg2 = kwarg2


@config.configurable
class ConfigurableClassWithNew(object):
"""A configurable class with __new__."""

def __new__(cls, kwarg1=None, kwarg2=None, return_none=False):
if return_none: # this will test __new__ and __init__ separation
return None
instance = super(ConfigurableClassWithNew, cls).__new__(cls)
instance.kwarg1 = kwarg1
return instance

def __init__(self, kwarg1=None, kwarg2=None, return_none=False):
self.kwarg1 = kwarg1
self.kwarg2 = kwarg2


class Meta(type):
def __call__(cls, post_new=None, *args, **kwargs):
instance = cls.__new__(cls, *args, **kwargs)
if post_new is not None: # this will allow to inspect post-new state
post_new(instance)
instance.__init__(*args, **kwargs)
return instance


@config.configurable
class ConfigurableClassWithMetaAndNew(object, metaclass=Meta):
"""A configurable class with metaclass and __new__"""

def __new__(cls, kwarg1=None, kwarg2=None, return_none=False):
if return_none: # this will test __new__ and __init__ separation
return None
instance = super(ConfigurableClassWithMetaAndNew, cls).__new__(cls)
instance.kwarg1 = kwarg1
return instance

def __init__(self, kwarg1=None, kwarg2=None, return_none=False):
self.kwarg1 = kwarg1
self.kwarg2 = kwarg2


@config.configurable
class ConfigurableClassWithMeta(object, metaclass=Meta):
"""A configurable class with metaclass"""

def __init__(self, kwarg1=None, kwarg2=None):
self.kwarg1 = kwarg1
self.kwarg2 = kwarg2


@config.configurable
class FaultyConfigurableClassWithMeta(object, metaclass=Meta):
"""A configurable class with metaclass"""
class Faulty(AssertionError): pass

def __init__(self, kwarg1=None, kwarg2=None):
raise FaultyConfigurableClassWithMeta.Faulty("Purposeful failure")


@config.configurable
class ConfigurableSubclass(ConfigurableClass):
"""A subclass of a configurable class."""
Expand Down Expand Up @@ -300,6 +360,16 @@ def __init__(self, kwarg1=None, kwarg2=None):
config.external_configurable(ExternalClass, 'module.ExternalConfigurable2')


class ExternalImportableClass(object):
"""External Importable Class"""
def __init__(self, kwarg1=None, kwarg2=None):
self.kwarg1 = kwarg1
self.kwarg2 = kwarg2

config.external_configurable(
ExternalImportableClass, 'ExternalImportableClass')


@config.configurable
class ConfigurableExternalSubclass(configurable_external_class):
"""Subclassing an external configurable object.
Expand Down Expand Up @@ -344,6 +414,23 @@ def implement_me(self):
config.external_configurable(ExternalAbstractConfigurableSubclass)


class ExternalConfigurableClassWithMetaAndNew(object, metaclass=Meta):
"""A configurable class with metaclass and __new__"""

def __new__(cls, kwarg1=None, kwarg2=None, return_none=False):
if return_none: # this will test __new__ and __init__ separation
return None
instance = super(ExternalConfigurableClassWithMetaAndNew, cls).__new__(cls)
instance.kwarg1 = kwarg1
return instance

def __init__(self, kwarg1=None, kwarg2=None, return_none=False):
self.kwarg1 = kwarg1
self.kwarg2 = kwarg2

config.external_configurable(ExternalConfigurableClassWithMetaAndNew)


class ConfigTest(absltest.TestCase):

def tearDown(self):
Expand Down Expand Up @@ -645,6 +732,154 @@ def testConfigurableClass(self):
instance = ConfigurableClass()
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')
try:
pickle.dumps(instance)
except AttributeError:
self.fail('Configurable class not picklable')
except pickle.PicklingError:
self.fail('Configurable class not picklable')

def testConfigurableClassWithNew(self):
config_str = """
ConfigurableClassWithNew.kwarg1 = 'statler'
ConfigurableClassWithNew.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
instance = ConfigurableClassWithNew()
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')
try:
pickle.dumps(instance)
except AttributeError:
self.fail('Configurable class with __new__ not picklable')
except pickle.PicklingError:
self.fail('Configurable class with __new__ not picklable')

def testConfigurableClassWithNewCanReturnOtherType(self):
config_str = """
ConfigurableClassWithNew.kwarg1 = 'statler'
ConfigurableClassWithNew.kwarg2 = 'waldorf'
ConfigurableClassWithNew.return_none = True
"""
config.parse_config(config_str)
instance = ConfigurableClassWithNew()
self.assertIsNotNone(instance)

config_str = """
ConfigurableClassWithNew.kwarg1 = 'statler'
ConfigurableClassWithNew.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
instance = ConfigurableClassWithNew(return_none=True)
self.assertIsNone(instance)

def testConfigurableClassWithMetaAndNew(self):
config_str = """
ConfigurableClassWithMetaAndNew.kwarg1 = 'statler'
ConfigurableClassWithMetaAndNew.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
instance = ConfigurableClassWithMetaAndNew()
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')
try:
pickle.dumps(instance)
except AttributeError:
self.fail('Configurable class with metaclass and __new__ not picklable')
except pickle.PicklingError:
self.fail('Configurable class with metaclass and __new__ not picklable')

def testConfigurableClassWithMetaAndNewAllowsInjections(self):
config_str = """
ConfigurableClassWithMetaAndNew.kwarg1 = 'statler'
ConfigurableClassWithMetaAndNew.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
class InjectionError(AssertionError): pass
def fail(_):
raise InjectionError("Purposeful fail")
try:
ConfigurableClassWithMetaAndNew(post_new=fail)
self.fail('ConfigurableClassWithMetaAndNew disallows injections')
except InjectionError:
pass # that's expected

def testConfigurableClassWithMetaAndNewSeparatesNewAndInit(self):
config_str = """
ConfigurableClassWithMetaAndNew.kwarg1 = 'statler'
ConfigurableClassWithMetaAndNew.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
def calls_new_with_no_injection_when_init_present(instance):
self.assertIsNone(instance.kwarg1)
self.assertFalse(hasattr(instance, 'kwarg2'))
instance = ConfigurableClassWithMetaAndNew(
post_new=calls_new_with_no_injection_when_init_present
)
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')

def testConfigurableClassWithMeta(self):
config_str = """
ConfigurableClassWithMeta.kwarg1 = 'statler'
ConfigurableClassWithMeta.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
instance = ConfigurableClassWithMeta()
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')
try:
pickle.dumps(instance)
except AttributeError:
self.fail('Configurable class with metaclass not picklable')
except pickle.PicklingError:
self.fail('Configurable class with metaclass not picklable')

def testConfigurableClassWithMetaAllowsInjections(self):
config_str = """
ConfigurableClassWithMeta.kwarg1 = 'statler'
ConfigurableClassWithMeta.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
class InjectionError(AssertionError): pass
def fail(_):
raise InjectionError("Purposeful fail")
try:
ConfigurableClassWithMeta(post_new=fail)
self.fail('ConfigurableClassWithMeta disallows injections')
except InjectionError:
pass # that's expected

def testConfigurableClassWithMetaSeparatesNewAndInit(self):
config_str = """
ConfigurableClassWithMeta.kwarg1 = 'statler'
ConfigurableClassWithMeta.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
def no_attrs(instance):
self.assertFalse(hasattr(instance, 'kwarg1'))
self.assertFalse(hasattr(instance, 'kwarg2'))
instance = ConfigurableClassWithMeta(post_new=no_attrs)
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')

def testFaultyConfigurableClassWithMeta(self):
config_str = """
FaultyConfigurableClassWithMeta.kwarg1 = 'statler'
FaultyConfigurableClassWithMeta.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
class InjectionError(AssertionError): pass
def fail(_):
raise InjectionError("Purposeful fail")
try:
FaultyConfigurableClassWithMeta(post_new=fail)
except InjectionError:
pass # that's expected
try:
FaultyConfigurableClassWithMeta()
except FaultyConfigurableClassWithMeta.Faulty:
pass # that's expected

def testConfigurableReferenceClassIdentityIsPreserved(self):
config_str = """
Expand Down Expand Up @@ -694,6 +929,13 @@ def testConfigurableSubclass(self):
self.assertIsNone(sub_instance.kwarg2)
self.assertEqual(sub_instance.kwarg3, 'thing')

try:
pickle.dumps(sub_instance)
except AttributeError:
self.fail('Configurable subclass not picklable')
except pickle.PicklingError:
self.fail('Configurable subclass not picklable')

def testConfigurableMethod(self):
config_str = """
configurable2.non_kwarg = @scoped/AbstractConfigurableSubclass()
Expand Down Expand Up @@ -724,6 +966,21 @@ def testExternalConfigurableClass(self):
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')

config_str = """
ConfigurableClass.kwarg1 = @ExternalImportableClass
ExternalImportableClass.kwarg1 = 'statler'
ExternalImportableClass.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
configurable_class = ConfigurableClass()
instance = configurable_class.kwarg1()
try:
pickle.dumps(instance)
except AttributeError:
self.fail('External configurable class not picklable')
except pickle.PicklingError as ex:
self.fail('External configurable class not picklable')

def testAbstractExternalConfigurableClass(self):
config_str = """
configurable2.non_kwarg = @ExternalAbstractConfigurableSubclass()
Expand Down Expand Up @@ -1706,6 +1963,54 @@ def testEmptyNestedIncludesAndImports(self):
[], ['TEST=1'], print_includes_and_imports=True)
self.assertListEqual(result, [])

def testExternalConfigurableClassWithMetaAndNew(self):
self.skipTest('To be discussed') # Was failing for legacy code as well
config_str = """
ExternalConfigurableClassWithMetaAndNew.kwarg1 = 'statler'
ExternalConfigurableClassWithMetaAndNew.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
instance = ExternalConfigurableClassWithMetaAndNew()
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')
try:
pickle.dumps(instance)
except AttributeError:
self.fail('External configurable class with meta and __new__ not picklable')
except pickle.PicklingError:
self.fail('External configurable class not picklable')

def testExternalConfigurableClassWithMetaAndNewAllowsInjections(self):
config_str = """
ExternalConfigurableClassWithMetaAndNew.kwarg1 = 'statler'
ExternalConfigurableClassWithMetaAndNew.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
class InjectionError(AssertionError): pass
def fail(_):
raise InjectionError("Purposeful fail")
try:
ExternalConfigurableClassWithMetaAndNew(post_new=fail)
self.fail('ExternalConfigurableClassWithMetaAndNew disallows injections')
except InjectionError:
pass # that's expected

def testExternalConfigurableClassWithMetaAndNewSeparatesNewAndInit(self):
self.skipTest('To be discussed') # Was failing for legacy code as well
config_str = """
ExternalConfigurableClassWithMetaAndNew.kwarg1 = 'statler'
ExternalConfigurableClassWithMetaAndNew.kwarg2 = 'waldorf'
"""
config.parse_config(config_str)
def calls_new_with_no_injection_when_init_present(instance):
self.assertIsNone(instance.kwarg1)
self.assertFalse(hasattr(instance, 'kwarg2'))
instance = ExternalConfigurableClassWithMetaAndNew(
post_new=calls_new_with_no_injection_when_init_present
)
self.assertEqual(instance.kwarg1, 'statler')
self.assertEqual(instance.kwarg2, 'waldorf')


if __name__ == '__main__':
absltest.main()