class IWeb(Protocol):
def get(self, suffix:str) -> str:
...
def _internal_stuff(self) -> None:
...Mocking objects
Public names
Since the main purpose of pymoq is to mock interfaces (aka protocols), we need a way to extract the public members of an interface. By convention, public members should not start with an underscore.
Methods
All methods whose name doesn’t start with an underscore should be included. The only public method in the following class is get.
All names are exposed through the dir method…
', '.join(dir(IWeb))'__abstractmethods__, __annotations__, __class__, __class_getitem__, __delattr__, __dict__, __dir__, __doc__, __eq__, __format__, __ge__, __getattribute__, __gt__, __hash__, __init__, __init_subclass__, __le__, __lt__, __module__, __ne__, __new__, __parameters__, __reduce__, __reduce_ex__, __repr__, __setattr__, __sizeof__, __slots__, __str__, __subclasshook__, __weakref__, _abc_impl, _internal_stuff, _is_protocol, _is_runtime_protocol, get'
… which can be filtered for names that don’t start with an underscore:
get_public_names
get_public_names (protocol:type)
Returns all names that are considered public from the given class
assert get_public_names(IWeb)==['get']Attributes
Attributes defined in protocol classes are not directly stored in the dir list. Instead, they are accessible in __annotations__. Note that protocol variables have to be defined at class level, not inside the __init__ method.
class IStore(Protocol):
store_id: int
name: str
_internal_key: int
def get(self, name:str) -> int:
...IStore.__annotations__{'store_id': int, 'name': str, '_internal_key': int}
get_public_attributes
get_public_attributes (protocol:<class'_ProtocolMeta'>)
Return a list of all attributes of the given protocol that are considered public.
get_public_attributes(IStore)['store_id', 'name']
assert 'store_id' in get_public_attributes(IStore)
assert 'name' in get_public_attributes(IStore)
assert not '_internal_key' in get_public_attributes(IStore)Construction from Protocol
Dynamic attribute access
Dynamic attribute access is possible by overriding the special method __getattr__. This method is called when a name is not found in the current instance of the class.
class Outer:
def __init__(self):
self.valid = 2
self.values = {'inner': 1}
def __getattr__(self, name: str):
print(f'Calling __getattr__("{name}")')
if name in self.values:
return self.values[name]
raise AttributeError(f'Name {name} not found in values dictionary')
o = Outer()Attribute is present in the class instance, so its accessed directly:
print(o.valid)2
Attribute is not present in the class instance, so __getattr__ is called:
print(o.inner)Calling __getattr__("inner")
1
try:
o.invalid
except Exception as e:
print(type(e), e)Calling __getattr__("invalid")
<class 'AttributeError'> Name invalid not found in values dictionary
The Mock object
The Mock object is the central class that the user of pymoq will interact with. It should be initialized with a protocol, setup function-mocks for all protocol-methods and handle the call redirection to the correct mock.
Mock
Mock (protocol:<class'_ProtocolMeta'>)
Initialize self. See help(type(self)) for accurate signature.
mock = Mock(IWeb)
assert str(mock) == 'Mock[IWeb]'
assert list(mock._function_mocks.keys()) == ['get']
mock._function_mocks{'get': <pymoq.mocking.functions.FunctionMock>}
When a function is called on a Mock, it should check whether that function is part of the underlyings protocol public interface. If not, throw an AttributeError. If yes, return the appropriate function mock.
mock = Mock(IWeb)
test_fail(lambda: mock.not_a_name)
assert isinstance(mock.get, FunctionMock)With this we can now build a working prototype of a mocked protocol.
mock = Mock(IWeb)
mock.get.setup(
ArgumentFunctionValidator(lambda a: isinstance(a, str), name='suffix', position=1)).returns(lambda self,suffix: f'suffix: {suffix}')assert mock.get('anyString') == 'suffix: anyString'
assert mock.get(1) is None
test_fail(lambda: mock.get())