marimo-logilab-helper 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- marimo_logilab_helper-0.1.1/PKG-INFO +36 -0
- marimo_logilab_helper-0.1.1/README.md +16 -0
- marimo_logilab_helper-0.1.1/pyproject.toml +57 -0
- marimo_logilab_helper-0.1.1/pyproject.toml.orig +42 -0
- marimo_logilab_helper-0.1.1/src/mo_logilab_helpers/__init__.py +47 -0
- marimo_logilab_helper-0.1.1/src/mo_logilab_helpers/mocks.py +62 -0
- marimo_logilab_helper-0.1.1/src/mo_logilab_helpers/others.py +14 -0
- marimo_logilab_helper-0.1.1/src/mo_logilab_helpers/solutions.py +60 -0
- marimo_logilab_helper-0.1.1/src/mo_logilab_helpers/tests.py +247 -0
- marimo_logilab_helper-0.1.1/src/mo_logilab_helpers/utils.py +69 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: marimo-logilab-helper
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Logilab Marimo helpers contains various functions used in Logilab training exercises implemented in Marimo notebooks
|
|
5
|
+
Keywords: marimo,notebook,education,logilab
|
|
6
|
+
Author: Logilab
|
|
7
|
+
Author-email: Logilab <contact@logilab.fr>
|
|
8
|
+
License: GNU Lesser General Public License v2 or later (LGPLv2+)
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Natural Language :: French
|
|
14
|
+
Classifier: Topic :: Education
|
|
15
|
+
Requires-Dist: marimo>=0.14
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Project-URL: Repository, https://forge.extranet.logilab.fr/open-source/logilab-marimo-helpers
|
|
18
|
+
Project-URL: Issues, https://forge.extranet.logilab.fr/open-source/logilab-marimo-helpers/-/boards#/
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
This package contains several functions that are used inside the
|
|
22
|
+
Marimo notebooks implementing Logilab training exercises. These
|
|
23
|
+
functions include:
|
|
24
|
+
|
|
25
|
+
- a function that can run a list of test functions and that displays
|
|
26
|
+
a table giving the result of each test function,
|
|
27
|
+
- several check functions that can be used inside the test functions to
|
|
28
|
+
verify a result,
|
|
29
|
+
- a function that can display, in a Markdown block, the solution stored
|
|
30
|
+
in a Python file,
|
|
31
|
+
- a function that can indicate if the Marimo notebook is running in a Web
|
|
32
|
+
Component (WASM) or in a Marimo regular server.
|
|
33
|
+
- a function that can mock the standard `open` function of Python when
|
|
34
|
+
the Marimo notebook is run in a Web Component (WASM); basically, instead
|
|
35
|
+
of opening a file on the disk (which is impossible from a program running
|
|
36
|
+
in the Web browser), it opens a file at an URL.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
This package contains several functions that are used inside the
|
|
2
|
+
Marimo notebooks implementing Logilab training exercises. These
|
|
3
|
+
functions include:
|
|
4
|
+
|
|
5
|
+
- a function that can run a list of test functions and that displays
|
|
6
|
+
a table giving the result of each test function,
|
|
7
|
+
- several check functions that can be used inside the test functions to
|
|
8
|
+
verify a result,
|
|
9
|
+
- a function that can display, in a Markdown block, the solution stored
|
|
10
|
+
in a Python file,
|
|
11
|
+
- a function that can indicate if the Marimo notebook is running in a Web
|
|
12
|
+
Component (WASM) or in a Marimo regular server.
|
|
13
|
+
- a function that can mock the standard `open` function of Python when
|
|
14
|
+
the Marimo notebook is run in a Web Component (WASM); basically, instead
|
|
15
|
+
of opening a file on the disk (which is impossible from a program running
|
|
16
|
+
in the Web browser), it opens a file at an URL.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "marimo-logilab-helper"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Logilab Marimo helpers contains various functions used in Logilab training exercises implemented in Marimo notebooks"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = ["marimo>=0.14"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"marimo",
|
|
10
|
+
"notebook",
|
|
11
|
+
"education",
|
|
12
|
+
"logilab",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Environment :: Web Environment",
|
|
16
|
+
"Programming Language :: Python",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Natural Language :: English",
|
|
19
|
+
"Natural Language :: French",
|
|
20
|
+
"Topic :: Education",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.license]
|
|
24
|
+
text = "GNU Lesser General Public License v2 or later (LGPLv2+)"
|
|
25
|
+
|
|
26
|
+
[[project.authors]]
|
|
27
|
+
name = "Logilab"
|
|
28
|
+
email = "contact@logilab.fr"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Repository = "https://forge.extranet.logilab.fr/open-source/logilab-marimo-helpers"
|
|
32
|
+
Issues = "https://forge.extranet.logilab.fr/open-source/logilab-marimo-helpers/-/boards#/"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest",
|
|
37
|
+
"ruff",
|
|
38
|
+
"twine",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.uv.build-backend]
|
|
42
|
+
module-name = "mo_logilab_helpers"
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
target-version = "py312"
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = [
|
|
49
|
+
"E4",
|
|
50
|
+
"E7",
|
|
51
|
+
"E9",
|
|
52
|
+
"F",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[build-system]
|
|
56
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
57
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "marimo-logilab-helper"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Logilab Marimo helpers contains various functions used in Logilab training exercises implemented in Marimo notebooks"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"marimo>=0.14",
|
|
9
|
+
]
|
|
10
|
+
license = {text = "GNU Lesser General Public License v2 or later (LGPLv2+)"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name="Logilab", email="contact@logilab.fr"},
|
|
13
|
+
]
|
|
14
|
+
keywords = ["marimo", "notebook", "education", "logilab"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Environment :: Web Environment",
|
|
17
|
+
"Programming Language :: Python",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Natural Language :: English",
|
|
20
|
+
"Natural Language :: French",
|
|
21
|
+
"Topic :: Education",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://forge.extranet.logilab.fr/open-source/logilab-marimo-helpers"
|
|
26
|
+
Issues = "https://forge.extranet.logilab.fr/open-source/logilab-marimo-helpers/-/boards#/"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["pytest", "ruff", "twine"]
|
|
30
|
+
|
|
31
|
+
[tool.uv.build-backend]
|
|
32
|
+
module-name = "mo_logilab_helpers"
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py312"
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
select = ["E4", "E7", "E9", "F"]
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
42
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package defining several useful functions that are used inside the Marimo
|
|
3
|
+
notebooks implementing Logilab training exercises.
|
|
4
|
+
|
|
5
|
+
These functions are:
|
|
6
|
+
|
|
7
|
+
``run_tests``
|
|
8
|
+
Run a list of test functions and display their result in the Marimo cell
|
|
9
|
+
output.
|
|
10
|
+
``check_value``
|
|
11
|
+
Inside a test function, checks a result is equal to a given value.
|
|
12
|
+
``check_type``
|
|
13
|
+
Inside a test function, checks a result has the given type.
|
|
14
|
+
``check_raise_exception``
|
|
15
|
+
Inside a test function, checks a piece of code raises an exception of a
|
|
16
|
+
given type
|
|
17
|
+
|
|
18
|
+
``display_solution``
|
|
19
|
+
Read the code of the solution stored inside a Python file and display it
|
|
20
|
+
in a Markdown block inside the Marimo cell output. The solution is only
|
|
21
|
+
displayed when a given switch is active.
|
|
22
|
+
``runs_in_web_component``
|
|
23
|
+
Return True if the Marimo notebook is run in a Web Component inside the
|
|
24
|
+
Web browser (some behaviours change in such a case and the Marimo notebook
|
|
25
|
+
must adapt to provide the same functionalities to the end-user).
|
|
26
|
+
``mock_open_function_when_running_in_web_component``
|
|
27
|
+
Dynamically change the standard `open` function (that reads a file on the
|
|
28
|
+
disk) with a custom function that emulates this function by reading a file
|
|
29
|
+
through an HTTP GET request. This change is necessary when the Marimo
|
|
30
|
+
notebook is run in a Web Component inside the Web browser because then the
|
|
31
|
+
standard `open` function can't work.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from .tests import run_tests, check_value, check_type, check_raise_exception
|
|
35
|
+
from .solutions import display_solution
|
|
36
|
+
from .others import runs_in_web_component
|
|
37
|
+
from .mocks import mock_open_function_when_running_in_web_component
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"run_tests",
|
|
41
|
+
"check_value",
|
|
42
|
+
"check_type",
|
|
43
|
+
"check_raise_exception",
|
|
44
|
+
"display_solution",
|
|
45
|
+
"runs_in_web_component",
|
|
46
|
+
"mock_open_function_when_running_in_web_component",
|
|
47
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module containing functions to mock some standard Python functions when the
|
|
3
|
+
Marimo notebook is run in a Web Component inside the Web browser.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
import marimo as mo
|
|
8
|
+
from .utils import get_message
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def mock_open_function_when_running_in_web_component(
|
|
12
|
+
globals_dict: dict[str, Any], lang: str = "en"
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
In a notebook run as a Web Component, replace the standard `open`
|
|
16
|
+
function with a custom function that emulates its behaviour but relies
|
|
17
|
+
on `urlopen` to read the data from an URL with an HTTP GET request.
|
|
18
|
+
|
|
19
|
+
This function replaces the standard `open` function in `globals()` with
|
|
20
|
+
our custom function. Note that the standard function is always available
|
|
21
|
+
in the `io` module. `globals_dict` parameter should be given `globals()`
|
|
22
|
+
object.
|
|
23
|
+
|
|
24
|
+
Be aware that our custom function can only emulate the reading of a file
|
|
25
|
+
with `open`. Writing a file can't be done through an HTTP GET request.
|
|
26
|
+
|
|
27
|
+
`lang` is only used to display a message in Marimo output.
|
|
28
|
+
"""
|
|
29
|
+
from urllib.request import urlopen
|
|
30
|
+
from types import MethodType
|
|
31
|
+
|
|
32
|
+
# Define our mock function
|
|
33
|
+
def open(file, mode="r", encoding=None):
|
|
34
|
+
resp = urlopen(file)
|
|
35
|
+
# Redefine `readline` on the stream returned by urlopen because it
|
|
36
|
+
# doesn't decode the read text (contrary to the standard `io text
|
|
37
|
+
# stream).
|
|
38
|
+
resp._readline = resp.readline
|
|
39
|
+
|
|
40
|
+
def readline(self):
|
|
41
|
+
return self._readline().decode(encoding or "utf-8")
|
|
42
|
+
|
|
43
|
+
resp.readline = MethodType(readline, resp)
|
|
44
|
+
# Return the customised stream
|
|
45
|
+
return resp
|
|
46
|
+
|
|
47
|
+
# Change the standard `open` function in the globals dict.
|
|
48
|
+
globals_dict["__builtin__"]["open"] = open
|
|
49
|
+
# Display a message in the Marimo cell output.
|
|
50
|
+
mo.output.replace(
|
|
51
|
+
mo.vstack(
|
|
52
|
+
[
|
|
53
|
+
mo.icon("ant-design:warning-filled", size=50, color="red"),
|
|
54
|
+
mo.md(
|
|
55
|
+
get_message("mock_open_1", lang=lang)
|
|
56
|
+
+ "\n\n"
|
|
57
|
+
+ get_message("mock_open_2", lang=lang)
|
|
58
|
+
).callout(kind="danger"),
|
|
59
|
+
],
|
|
60
|
+
align="start",
|
|
61
|
+
)
|
|
62
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module containing various other useful functions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def runs_in_web_component() -> bool:
|
|
7
|
+
"""
|
|
8
|
+
Return True if the Marimo notebook is running in a Web Component inside
|
|
9
|
+
the Web browser (instead of running in the Marimo server onto a regular
|
|
10
|
+
operating system).
|
|
11
|
+
"""
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
return sys.platform == "emscripten"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module defining the functions that can be used to display the solution
|
|
3
|
+
(content of a Python file stored near the Marimo exercice notebook.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import marimo as mo
|
|
8
|
+
from .others import runs_in_web_component
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def display_solution(
|
|
12
|
+
switch_obj, sol_filename: str | Path, markdown: bool = False
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Display the Python code of the solution stored inside ``sol_filename`` in
|
|
16
|
+
the output of the Marimo cell.
|
|
17
|
+
|
|
18
|
+
The solution is only shown when the Marimo switch ``switch_obj`` is
|
|
19
|
+
active.
|
|
20
|
+
|
|
21
|
+
If ``markdown`` is ``True``, the content of the solution is directly
|
|
22
|
+
displayed, else it is inserted in a code block marker.
|
|
23
|
+
"""
|
|
24
|
+
sol_code: list[str] = []
|
|
25
|
+
try:
|
|
26
|
+
if runs_in_web_component():
|
|
27
|
+
# We are running inside the Web browser and must use `urlopen` to
|
|
28
|
+
# read the file containing the solution.
|
|
29
|
+
from urllib.request import urlopen
|
|
30
|
+
|
|
31
|
+
with urlopen(str(sol_filename)) as stream:
|
|
32
|
+
sol_code = [
|
|
33
|
+
line.decode("utf-8") for line in stream.readlines()
|
|
34
|
+
]
|
|
35
|
+
else:
|
|
36
|
+
with open(sol_filename) as stream:
|
|
37
|
+
sol_code = stream.readlines()
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
mode = "in file" if not runs_in_web_component() else "at URL"
|
|
40
|
+
sol_code = [
|
|
41
|
+
f"# Can't access the solution {mode}: {sol_filename}\n",
|
|
42
|
+
"\n",
|
|
43
|
+
f"# Encountered the error: {exc.__class__.__name__} {exc}\n",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
if switch_obj.value:
|
|
47
|
+
mo.output.replace(
|
|
48
|
+
mo.vstack(
|
|
49
|
+
[
|
|
50
|
+
switch_obj,
|
|
51
|
+
mo.md(
|
|
52
|
+
"".join(sol_code)
|
|
53
|
+
if markdown
|
|
54
|
+
else f"```python3\n{"".join(sol_code)}\n```\n"
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
mo.output.replace(switch_obj)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module defining the functions that can be used to run tests in the Marimo
|
|
3
|
+
notebook and display their results in the Marimo cell output.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional, Callable, Any, cast
|
|
7
|
+
import types
|
|
8
|
+
import numbers
|
|
9
|
+
from traceback import format_exc
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
import marimo as mo
|
|
12
|
+
from .utils import get_message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CheckError(Exception):
|
|
16
|
+
"""
|
|
17
|
+
Custom exception raised by the check functions (see below).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_tests(
|
|
24
|
+
test_functions_list: list[Callable[..., None]], lang: str = "en"
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Run the functions in ``test_functions_list`` list and display their
|
|
28
|
+
result in a nice grid in the output of the Marimo cell. The potential
|
|
29
|
+
tracebacks are printed (in the printing zone of the Marimo cell).
|
|
30
|
+
|
|
31
|
+
Each function can use the check functions (also defined in this module)
|
|
32
|
+
to verify an expected result.
|
|
33
|
+
"""
|
|
34
|
+
results_data = []
|
|
35
|
+
for test_function in test_functions_list:
|
|
36
|
+
doc_lines = [
|
|
37
|
+
lin.strip()
|
|
38
|
+
for lin in (test_function.__doc__ or "").strip().split("\n")
|
|
39
|
+
]
|
|
40
|
+
doc_first_para = (
|
|
41
|
+
doc_lines[: doc_lines.index("")]
|
|
42
|
+
if "" in doc_lines
|
|
43
|
+
else doc_lines[:]
|
|
44
|
+
)
|
|
45
|
+
data_row: list[Optional[str]] = [
|
|
46
|
+
test_function.__name__,
|
|
47
|
+
" ".join(doc_first_para),
|
|
48
|
+
]
|
|
49
|
+
results_data.append(data_row)
|
|
50
|
+
try:
|
|
51
|
+
test_function()
|
|
52
|
+
data_row.append(f"✅️ {get_message('ok', lang=lang)}")
|
|
53
|
+
data_row.append(None)
|
|
54
|
+
except CheckError:
|
|
55
|
+
data_row.append(f"⚠️ **{get_message('failed', lang=lang)}**")
|
|
56
|
+
data_row.append(format_exc())
|
|
57
|
+
except Exception:
|
|
58
|
+
data_row.append(f"❌️ **{get_message('error', lang=lang)}**")
|
|
59
|
+
data_row.append(format_exc())
|
|
60
|
+
|
|
61
|
+
output_rows = []
|
|
62
|
+
for name, desc, status, details in results_data:
|
|
63
|
+
output_rows.append(
|
|
64
|
+
mo.hstack(
|
|
65
|
+
[
|
|
66
|
+
mo.vstack(
|
|
67
|
+
[
|
|
68
|
+
mo.md(f"**{name}**"),
|
|
69
|
+
mo.md(desc),
|
|
70
|
+
],
|
|
71
|
+
gap=0,
|
|
72
|
+
),
|
|
73
|
+
mo.md(status),
|
|
74
|
+
],
|
|
75
|
+
widths=[3, 1],
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
if details is not None:
|
|
79
|
+
print(f"*** {name}\n")
|
|
80
|
+
print(details)
|
|
81
|
+
|
|
82
|
+
mo.output.replace(mo.vstack(output_rows, heights="equal"))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def check_value(
|
|
86
|
+
result: Any,
|
|
87
|
+
expected_value: Any,
|
|
88
|
+
precision: int = 6,
|
|
89
|
+
message: Optional[str] = None,
|
|
90
|
+
**context: dict[str, Any],
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
Check ``result`` is equal to the ``expected_value``.
|
|
94
|
+
|
|
95
|
+
If ``result`` is a number, the equality is tested with the specified
|
|
96
|
+
``precision`` (difference less than 10^-<precision>). It is possible to
|
|
97
|
+
set ``precision`` to ``None`` to avoid this behaviour and check for a
|
|
98
|
+
strict equality.
|
|
99
|
+
|
|
100
|
+
``message`` is the text written at the beginning of the error message.
|
|
101
|
+
|
|
102
|
+
Additional arguments with their values can be provided to describe the
|
|
103
|
+
context of the test (e.g. values used to run the function that produced
|
|
104
|
+
``result``).
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(result, numbers.Number) and precision is not None:
|
|
107
|
+
power = -precision
|
|
108
|
+
check_test = abs(result - expected_value) < 10**power
|
|
109
|
+
else:
|
|
110
|
+
check_test = result == expected_value
|
|
111
|
+
if not check_test:
|
|
112
|
+
str_context = (
|
|
113
|
+
(
|
|
114
|
+
"\nContext: "
|
|
115
|
+
+ " ; ".join(
|
|
116
|
+
f"{key}={repr(val)}" for key, val in context.items()
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
if context
|
|
120
|
+
else ""
|
|
121
|
+
)
|
|
122
|
+
msg = (
|
|
123
|
+
message
|
|
124
|
+
if message
|
|
125
|
+
else "The result is different from the expected value:"
|
|
126
|
+
)
|
|
127
|
+
raise CheckError(
|
|
128
|
+
f"{msg}\n {repr(result)}\nis different from\n"
|
|
129
|
+
f" {repr(expected_value)}{str_context}\n"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def check_type(
|
|
134
|
+
result: Any,
|
|
135
|
+
expected_type: type | tuple[type, ...] | types.UnionType,
|
|
136
|
+
message: Optional[str] = None,
|
|
137
|
+
**context: dict[str, Any],
|
|
138
|
+
):
|
|
139
|
+
"""
|
|
140
|
+
Check ``result`` has the ``expected_type`` type.
|
|
141
|
+
|
|
142
|
+
``expected_type`` can be a single type, a tuple of types or an union of
|
|
143
|
+
types (see documentation of the standard ``isinstance`` function).
|
|
144
|
+
|
|
145
|
+
``message`` is the text written at the beginning of the error message.
|
|
146
|
+
|
|
147
|
+
Additional arguments with their values can be provided to describe the
|
|
148
|
+
context of the test (e.g. values used to run the function that produced
|
|
149
|
+
``result``).
|
|
150
|
+
"""
|
|
151
|
+
exp_types = expected_type
|
|
152
|
+
if not isinstance(expected_type, (tuple, types.UnionType)):
|
|
153
|
+
exp_types = (expected_type,)
|
|
154
|
+
if not isinstance(result, exp_types):
|
|
155
|
+
str_types = (
|
|
156
|
+
str(exp_types)
|
|
157
|
+
if isinstance(exp_types, types.UnionType)
|
|
158
|
+
else " | ".join(
|
|
159
|
+
(f"{typ.__module__}." if typ.__module__ != "builtins" else "")
|
|
160
|
+
+ typ.__name__
|
|
161
|
+
for typ in cast(tuple[type, ...], exp_types)
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
str_context = (
|
|
165
|
+
(
|
|
166
|
+
"\nContext: "
|
|
167
|
+
+ " ; ".join(
|
|
168
|
+
f"{key}={repr(val)}" for key, val in context.items()
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
if context
|
|
172
|
+
else ""
|
|
173
|
+
)
|
|
174
|
+
msg = (
|
|
175
|
+
message
|
|
176
|
+
if message
|
|
177
|
+
else "The result doesn't have the expected type:"
|
|
178
|
+
)
|
|
179
|
+
raise CheckError(
|
|
180
|
+
f"{msg}\n {repr(result)}\nhas not a type in\n {str_types}"
|
|
181
|
+
f"{str_context}\n"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@contextmanager
|
|
186
|
+
def check_raise_exception(
|
|
187
|
+
exception_type,
|
|
188
|
+
message: Optional[str] = None,
|
|
189
|
+
**context: dict[str, Any],
|
|
190
|
+
):
|
|
191
|
+
"""
|
|
192
|
+
Context manager that verifies the code it contains raises an exception of
|
|
193
|
+
``expected_type`` type.
|
|
194
|
+
|
|
195
|
+
``expected_type`` can be a single type, a tuple of types or an union of
|
|
196
|
+
types (see documentation of the standard ``isinstance`` function).
|
|
197
|
+
|
|
198
|
+
``message`` is the text written at the beginning of the error message.
|
|
199
|
+
|
|
200
|
+
Additional arguments with their values can be provided to describe the
|
|
201
|
+
context of the test (e.g. values used to run the function that raised the
|
|
202
|
+
exception).
|
|
203
|
+
"""
|
|
204
|
+
exc_types = exception_type
|
|
205
|
+
if not isinstance(exception_type, (tuple, types.UnionType)):
|
|
206
|
+
exc_types = (exception_type,)
|
|
207
|
+
error = False
|
|
208
|
+
exc_obj: Optional[Exception] = None
|
|
209
|
+
try:
|
|
210
|
+
yield
|
|
211
|
+
error = True
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
if not isinstance(exc, exc_types):
|
|
214
|
+
error = True
|
|
215
|
+
exc_obj = exc
|
|
216
|
+
finally:
|
|
217
|
+
if error:
|
|
218
|
+
str_exc_types = (
|
|
219
|
+
str(exc_types)
|
|
220
|
+
if isinstance(exc_types, types.UnionType)
|
|
221
|
+
else " | ".join(excp.__name__ for excp in exc_types)
|
|
222
|
+
)
|
|
223
|
+
str_context = (
|
|
224
|
+
(
|
|
225
|
+
"\nContext: "
|
|
226
|
+
+ " ; ".join(
|
|
227
|
+
f"{key}={repr(val)}" for key, val in context.items()
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
if context
|
|
231
|
+
else ""
|
|
232
|
+
)
|
|
233
|
+
if exc_obj is not None:
|
|
234
|
+
msg = (
|
|
235
|
+
message
|
|
236
|
+
if message
|
|
237
|
+
else "An unexpected exception has been raised:"
|
|
238
|
+
)
|
|
239
|
+
raise CheckError(
|
|
240
|
+
f"{msg}\n {repr(exc_obj)}\nhas not an exception type in:"
|
|
241
|
+
f"\n {str_exc_types}{str_context}\n"
|
|
242
|
+
)
|
|
243
|
+
msg = message if message else "No exception has been raised:"
|
|
244
|
+
raise CheckError(
|
|
245
|
+
f"{msg}\n\nAn exception was expected with a type in:\n"
|
|
246
|
+
f" {str_exc_types}{str_context}\n"
|
|
247
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module containing the localised messages that will be displayed in the Marimo
|
|
3
|
+
notebook.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
_MESSAGES: dict[str, dict[str, str]] = {
|
|
7
|
+
"en": {
|
|
8
|
+
"ok": "ok",
|
|
9
|
+
"failed": "Failed test",
|
|
10
|
+
"error": "Execution error",
|
|
11
|
+
"mock_open_1": (
|
|
12
|
+
"This notebook is executed as a Web Component inside the browser. "
|
|
13
|
+
"Python is not executed in regular operating system on a computer "
|
|
14
|
+
"but inside the Web browser. As such, the standard `open` "
|
|
15
|
+
"function can't be used to read files because, for security "
|
|
16
|
+
"reasons, the Web browser can't access to the computer "
|
|
17
|
+
"filesystem. Therefore, we have replaced the `open` function with "
|
|
18
|
+
"a custom function that can get a file at a given URL and that "
|
|
19
|
+
"emulates the behaviour of the standard function."
|
|
20
|
+
),
|
|
21
|
+
"mock_open_2": (
|
|
22
|
+
"Please note that we can't emulate all the behaviour of the "
|
|
23
|
+
"standard `open` function because getting a document by "
|
|
24
|
+
"requesting an URL is quite different from reading a file on a "
|
|
25
|
+
"disk. Our custom function has only three parameters: `filename`, "
|
|
26
|
+
"`mode` and `encoding`. This is enough to cover most of the use "
|
|
27
|
+
"cases. Be aware that our custom function can **only read files** "
|
|
28
|
+
'(`mode="r"`) because, as you might suspect, it is impossible '
|
|
29
|
+
"to write files through a Web GET request."
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
"fr": {
|
|
33
|
+
"ok": "ok",
|
|
34
|
+
"failed": "Échec du test",
|
|
35
|
+
"error": "Erreur d'exécution",
|
|
36
|
+
"mock_open_1": (
|
|
37
|
+
"Ce calepin est exécuté comme Composant Web (*Web Component*) "
|
|
38
|
+
"dans le navigateur Web. Python n'est pas exécuté sur le "
|
|
39
|
+
"système d'exploitation classique d'un ordinateur mais dans le "
|
|
40
|
+
"navigateur Web. Ainsi, la fonction standard `open` ne peut pas "
|
|
41
|
+
"être utilisée pour lire des fichiers car le navigateur, pour "
|
|
42
|
+
"des raisons de sécurité, n'a pas accès au système de fichiers "
|
|
43
|
+
"de l'ordinateur. Par conséquent, nous avons remplacé la "
|
|
44
|
+
"fonction `open` par une fonction personnalisée qui récupère un "
|
|
45
|
+
"fichier à une URL donnée et qui émule le comportement de la "
|
|
46
|
+
"fonction standard."
|
|
47
|
+
),
|
|
48
|
+
"mock_open_2": (
|
|
49
|
+
"Notez que nous ne pouvons pas émuler le comportement complet de "
|
|
50
|
+
"la fonction standard `open` car récupérer un document en "
|
|
51
|
+
"requêtant une URL est très différent de lire un fichier sur "
|
|
52
|
+
"un disque. Notre fonction personnalisée a seulement trois "
|
|
53
|
+
"paramètres: `filename`, `mode` et `encoding`. Cela suffit pour "
|
|
54
|
+
"couvrir la plupart des cas d'utilisation. Soyez bien conscients "
|
|
55
|
+
"que notre fonction personnalisée peut uniquement **lire des "
|
|
56
|
+
'fichiers** (`mode="r"`) car, comme vous vous en doutez, il est '
|
|
57
|
+
"impossible d'écrire des fichiers au travers d'une requête Web "
|
|
58
|
+
"de type GET."
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_message(message_id, lang="en"):
|
|
65
|
+
"""
|
|
66
|
+
Get the message corresponding to `message_id` in the `lang` language.
|
|
67
|
+
"""
|
|
68
|
+
localised_msgs = _MESSAGES[lang]
|
|
69
|
+
return localised_msgs[message_id]
|