Skip to content

Commit c866652

Browse files
committed
Now works in both MicroPython and Pyodide. Updated code, tests and docs.
1 parent 2891761 commit c866652

File tree

6 files changed

+75
-31
lines changed

6 files changed

+75
-31
lines changed

index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
<script type="module" src="https://pyscript.net/releases/2024.6.1/core.js"></script>
1616
</head>
1717
<body>
18-
<h1>MicroMock test suite</h1>
18+
<h1>MicroMock test suite (MicroPython)</h1>
19+
<p><a href="./index2.html">Pyodide version</a></p>
1920
<script type="mpy" src="./main.py" config="./config.json" terminal async></script>
2021
</body>
2122
</html>

index2.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<script src="/mini-coi.js" scope="./"></script>
5+
<title>PyScript Application Template</title>
6+
7+
<!-- Recommended meta tags -->
8+
<meta charset="UTF-8">
9+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
10+
11+
<!-- PyScript CSS -->
12+
<link rel="stylesheet" href="https://pyscript.net/releases/2024.6.1/core.css">
13+
14+
<!-- This script tag bootstraps PyScript -->
15+
<script type="module" src="https://pyscript.net/releases/2024.6.1/core.js"></script>
16+
</head>
17+
<body>
18+
<h1>MicroMock test suite (Pyodide)</h1>
19+
<p><a href="./index.html">MicroPython version</a></p>
20+
<script type="py" src="./main.py" config="./config.json" terminal async></script>
21+
</body>
22+
</html>

tests/test_asyncmock.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
exacty the same as unittest.mock.AsyncMock).
44
"""
55

6+
import sys
67
import upytest
78
from umock import AsyncMock
89

910

11+
#: A flag to show if MicroPython is the current Python interpreter.
12+
is_micropython = "micropython" in sys.version.lower()
13+
14+
1015
def test_init_async_mock():
1116
"""
1217
A plain AsyncMock object can be created with no arguments. Accessing
@@ -46,9 +51,8 @@ def test_init_async_mock_with_spec_from_list():
4651
def test_init_async_mock_with_spec_from_object():
4752
"""
4853
An AsyncMock object should be created with the specified attributes derived
49-
from the referenced instance. The AsyncMock's __class__ should be set to
50-
that of the spec object's. Accessing arbitrary attributes not on the class
51-
should raise an AttributeError.
54+
from the referenced instance. Accessing arbitrary attributes not on the
55+
object should raise an AttributeError.
5256
5357
If an arbitrary attribute is subqeuently added to the mock object, it
5458
should be accessible as per normal Python behaviour.
@@ -66,7 +70,7 @@ class TestClass:
6670
mock, "z"
6771
), "AsyncMock object has unexpected 'z' attribute."
6872
assert (
69-
mock.__class__ == TestClass
73+
mock.__class__ == AsyncMock
7074
), "AsyncMock object has unexpected class."
7175
mock.z = "test"
7276
assert (
@@ -144,7 +148,7 @@ async def test_init_async_mock_with_exception_instance_side_effect():
144148
with upytest.raises(ValueError) as expected:
145149
await mock()
146150
assert (
147-
str(expected.exception.value) == "test"
151+
str(expected.exception) == "test"
148152
), "Exception message not as expected."
149153

150154

@@ -161,7 +165,10 @@ async def test_init_async_mock_with_iterable_side_effect():
161165
assert await mock() == 3, "Third call did not return 3."
162166
with upytest.raises(RuntimeError) as expected:
163167
await mock()
164-
assert expected.exception.value == "generator raised StopIteration"
168+
if is_micropython:
169+
assert str(expected.exception) == "generator raised StopIteration"
170+
else:
171+
assert str(expected.exception) == "coroutine raised StopIteration"
165172

166173

167174
async def test_init_async_mock_with_invalid_side_effect():

tests/test_mock.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ def test_init_mock_with_spec_from_list():
4242
def test_init_mock_with_spec_from_object():
4343
"""
4444
A Mock object should be created with the specified attributes derived from
45-
the referenced instance. The Mock's __class__ should be set to that of the
46-
spec object's. Accessing arbitrary attributes not on the class should raise
47-
an AttributeError.
45+
the referenced instance. Accessing arbitrary attributes not on the object
46+
should raise an AttributeError.
4847
4948
If an arbitrary attribute is subqeuently added to the mock object, it
5049
should be accessible as per normal Python behaviour.
@@ -59,17 +58,16 @@ class TestClass:
5958
assert hasattr(mock, "x"), "Mock object missing 'x' attribute."
6059
assert hasattr(mock, "y"), "Mock object missing 'y' attribute."
6160
assert not hasattr(mock, "z"), "Mock object has unexpected 'z' attribute."
62-
assert mock.__class__ == TestClass, "Mock object has unexpected class."
61+
assert mock.__class__ == Mock, "Mock object has unexpected class."
6362
mock.z = "test"
6463
assert mock.z == "test", "Mock object attribute 'z' not set correctly."
6564

6665

6766
def test_init_mock_with_spec_from_class():
6867
"""
6968
A Mock object should be created with the specified attributes derived from
70-
the referenced class. Since this is a class spec, the Mock's __class__
71-
remains as Mock. Accessing arbitrary attributes not on the class should
72-
raise an AttributeError.
69+
the referenced class. Accessing arbitrary attributes not on the class
70+
should raise an AttributeError.
7371
7472
If an arbitrary attribute is subqeuently added to the mock object, it
7573
should be accessible as per normal Python behaviour.
@@ -128,7 +126,7 @@ def test_init_mock_with_exception_instance_side_effect():
128126
with upytest.raises(ValueError) as expected:
129127
mock()
130128
assert (
131-
str(expected.exception.value) == "test"
129+
str(expected.exception) == "test"
132130
), "Exception message not as expected."
133131

134132

tests/test_patch_target.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,8 @@ def test_patch_target_module():
2323
old_module = patch_target(target, mock_module)
2424
# The module is replaced with the mock module.
2525
assert sys.modules[target] is mock_module
26-
# The imported module is also the mock module.
27-
from tests.a_package import another_module
28-
29-
assert another_module is mock_module
3026
# The original module is returned from the patch.
31-
assert old_module.__name__ == target.replace(".", "/")
27+
assert old_module.__name__.replace(".", "/") == target.replace(".", "/")
3228
# Restore the old module.
3329
patch_target(target, old_module)
3430

umock.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030

3131
__all__ = ["Mock", "AsyncMock", "patch"]
3232

33+
34+
#: A flag to show if MicroPython is the current Python interpreter.
35+
is_micropython = "micropython" in sys.version.lower()
36+
37+
3338
#: Attributes of the Mock class that should be handled as "normal" attributes
3439
#: rather than treated as mocked attributes.
3540
_RESERVED_MOCK_ATTRIBUTES = ("side_effect", "return_value")
@@ -40,7 +45,30 @@ def is_awaitable(obj):
4045
Returns a boolean indication if the passed in obj is an awaitable
4146
function. (MicroPython treats awaitables as generator functions.)
4247
"""
43-
return inspect.isgeneratorfunction(obj)
48+
if is_micropython:
49+
return inspect.isgeneratorfunction(obj)
50+
return inspect.iscoroutinefunction(obj)
51+
52+
53+
def import_module(module_path):
54+
"""
55+
Import the referenced module in a way that works with both MicroPython and
56+
Pyodide. The module_path should be a dotted string representing the module
57+
to import.
58+
"""
59+
if is_micropython:
60+
file_path = module_path.replace(".", "/")
61+
module = __import__(file_path)
62+
return module
63+
else:
64+
module_name = module_path.split(".")[-1]
65+
module = __import__(
66+
module_path,
67+
fromlist=[
68+
module_name,
69+
],
70+
)
71+
return module
4472

4573

4674
class Mock:
@@ -112,9 +140,6 @@ class or instance) that acts as the specification for the mock
112140
or is_awaitable(getattr(spec, name)) # no awaitables
113141
)
114142
]
115-
if type(spec) is not type:
116-
# Set the mock object's class to that of the spec object.
117-
self.__class__ = type(spec)
118143
for name in self._spec:
119144
# Create a new mock object for each attribute in the spec.
120145
setattr(self, name, Mock())
@@ -372,9 +397,6 @@ class or instance) that acts as the specification for the mock
372397
or is_awaitable(getattr(spec, name)) # no awaitables
373398
)
374399
]
375-
if type(spec) is not type:
376-
# Set the mock object's class to that of the spec object.
377-
self.__class__ = type(spec)
378400
for name in self._spec:
379401
# Create a new mock object for each attribute in the spec.
380402
setattr(self, name, Mock())
@@ -631,8 +653,7 @@ def patch_target(target, replacement):
631653
if target in sys.modules:
632654
old_module = sys.modules[target]
633655
else:
634-
module_path = target.replace(".", "/")
635-
old_module = __import__(module_path)
656+
old_module = import_module(target)
636657
sys.modules[target] = replacement
637658
return old_module
638659
# There IS a colon in the target, so split the target into module and
@@ -643,8 +664,7 @@ def patch_target(target, replacement):
643664
# Get the parent module of the target.
644665
parent = sys.modules.get(module_name)
645666
if not parent:
646-
module_path = module_name.replace(".", "/")
647-
parent = __import__(module_path)
667+
parent = import_module(module_name)
648668
# Traverse the module path to get the parent object of the target.
649669
parts = attributes.split(".")
650670
for part in parts[:-1]:

0 commit comments

Comments
 (0)