generated from daniil-berg/boilerplate-py
	✨ Implement simple generic mixin for saving a single type argument
This commit is contained in:
		
							
								
								
									
										31
									
								
								src/marshmallow_generic/_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/marshmallow_generic/_util.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					from typing import Any, Generic, Optional, TypeVar, get_args, get_origin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_T = TypeVar("_T")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GenericInsightMixin(Generic[_T]):
 | 
				
			||||||
 | 
					    _type_arg: Optional[type[_T]] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def __init_subclass__(cls, **kwargs: Any) -> None:
 | 
				
			||||||
 | 
					        """Saves the type argument in the `_type_arg` class attribute."""
 | 
				
			||||||
 | 
					        super().__init_subclass__(**kwargs)
 | 
				
			||||||
 | 
					        for base in cls.__orig_bases__:  # type: ignore[attr-defined]
 | 
				
			||||||
 | 
					            origin = get_origin(base)
 | 
				
			||||||
 | 
					            if origin is None or not issubclass(origin, GenericInsightMixin):
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            type_arg = get_args(base)[0]
 | 
				
			||||||
 | 
					            # Do not set the attribute for GENERIC subclasses!
 | 
				
			||||||
 | 
					            if not isinstance(type_arg, TypeVar):
 | 
				
			||||||
 | 
					                cls._type_arg = type_arg
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def _get_type_arg(cls) -> type[_T]:
 | 
				
			||||||
 | 
					        """Returns the type argument of the class (if specified)."""
 | 
				
			||||||
 | 
					        if cls._type_arg is None:
 | 
				
			||||||
 | 
					            raise AttributeError(
 | 
				
			||||||
 | 
					                f"{cls.__name__} is generic; type argument unspecified"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        return cls._type_arg
 | 
				
			||||||
							
								
								
									
										53
									
								
								tests/test__util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								tests/test__util.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					from typing import Generic, TypeVar
 | 
				
			||||||
 | 
					from unittest import TestCase
 | 
				
			||||||
 | 
					from unittest.mock import MagicMock, patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from marshmallow_generic import _util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GenericInsightMixinTestCase(TestCase):
 | 
				
			||||||
 | 
					    @patch.object(_util, "super")
 | 
				
			||||||
 | 
					    def test___init_subclass__(self, mock_super: MagicMock) -> None:
 | 
				
			||||||
 | 
					        mock_super_meth = MagicMock()
 | 
				
			||||||
 | 
					        mock_super.return_value = MagicMock(__init_subclass__=mock_super_meth)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Should be `None` by default:
 | 
				
			||||||
 | 
					        self.assertIsNone(_util.GenericInsightMixin._type_arg)  # type: ignore[misc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If the mixin type argument was not specified (still generic),
 | 
				
			||||||
 | 
					        # ensure that the attribute remains `None` on the subclass:
 | 
				
			||||||
 | 
					        t = TypeVar("t")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class Foo:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class Bar(Generic[t]):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class TestSchema1(Bar[str], _util.GenericInsightMixin[t]):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIsNone(TestSchema1._type_arg)  # type: ignore[misc]
 | 
				
			||||||
 | 
					        mock_super.assert_called_once()
 | 
				
			||||||
 | 
					        mock_super_meth.assert_called_once_with()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        mock_super.reset_mock()
 | 
				
			||||||
 | 
					        mock_super_meth.reset_mock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If the mixin type argument was specified,
 | 
				
			||||||
 | 
					        # ensure it was assigned to the attribute on the child class:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class TestSchema2(Bar[str], _util.GenericInsightMixin[Foo]):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIs(Foo, TestSchema2._type_arg)  # type: ignore[misc]
 | 
				
			||||||
 | 
					        mock_super.assert_called_once()
 | 
				
			||||||
 | 
					        mock_super_meth.assert_called_once_with()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test__get_type_arg(self) -> None:
 | 
				
			||||||
 | 
					        with self.assertRaises(AttributeError):
 | 
				
			||||||
 | 
					            _util.GenericInsightMixin._get_type_arg()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _type = object()
 | 
				
			||||||
 | 
					        with patch.object(_util.GenericInsightMixin, "_type_arg", new=_type):
 | 
				
			||||||
 | 
					            self.assertIs(_type, _util.GenericInsightMixin._get_type_arg())
 | 
				
			||||||
		Reference in New Issue
	
	Block a user