Skip to content

Commit 7dd2909

Browse files
authored
Merge branch 'develop' into dev-define-engines-abc
2 parents d91518a + f16c232 commit 7dd2909

File tree

8 files changed

+247
-18
lines changed

8 files changed

+247
-18
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# This workflow will perform code type checking using mypy
2+
3+
name: mypy type checking
4+
5+
on:
6+
push:
7+
branches: [ develop, pre-release, master, main ]
8+
pull_request:
9+
branches: [ develop, pre-release, master, main ]
10+
11+
jobs:
12+
13+
build:
14+
15+
runs-on: ubuntu-22.04
16+
17+
strategy:
18+
matrix:
19+
python-version: ["3.8", "3.9", "3.10", "3.11"]
20+
21+
steps:
22+
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v3
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
28+
- name: Checkout repository
29+
uses: actions/checkout@v3
30+
31+
- name: Setup mypy
32+
run: |
33+
pip install mypy
34+
35+
- name: Perform type checking
36+
run: |
37+
mypy --install-types --non-interactive --follow-imports=skip \
38+
tiatoolbox/__init__.py \
39+
tiatoolbox/__main__.py \
40+
tiatoolbox/utils/exceptions.py

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ repos:
5959
- id: rst-directive-colons # Detect mistake of rst directive not ending with double colon.
6060
- id: rst-inline-touching-normal # Detect mistake of inline code touching normal text in rst.
6161
- repo: https://github.com/psf/black
62-
rev: 23.9.1 # Replace with any tag/version: https://github.com/psf/black/tags
62+
rev: 23.10.0 # Replace with any tag/version: https://github.com/psf/black/tags
6363
hooks:
6464
- id: black
6565
language_version: python3 # Should be a command that runs python3.+
@@ -68,7 +68,7 @@ repos:
6868
language: python
6969
- repo: https://github.com/astral-sh/ruff-pre-commit
7070
# Ruff version.
71-
rev: v0.0.292
71+
rev: v0.1.1
7272
hooks:
7373
- id: ruff
7474
args: [--fix, --exit-non-zero-on-fix]

requirements/requirements_dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ black>=23.3.0
44
coverage>=7.0.0
55
docutils>=0.18.1
66
jinja2>=3.0.3, <3.1.0
7+
mypy>=1.6.1
78
pip>=22.3
89
poetry-bumpversion>=0.3.1
910
pre-commit>=2.20.0

tests/test_init.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,14 @@ def test_lazy_import() -> None:
139139
)
140140

141141
assert "exceptions" in sys.modules
142+
143+
144+
def test_lazy_import_module_not_found() -> None:
145+
"""'Test lazy import for ModuleNotFoundError."""
146+
from tiatoolbox import _lazy_import
147+
148+
with pytest.raises(ModuleNotFoundError):
149+
_lazy_import(
150+
"nonexistent_module",
151+
Path(__file__).parent.parent / "tiatoolbox",
152+
)

tests/test_utils.py

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,7 +1638,7 @@ def test_patch_pred_store() -> None:
16381638
"other": "other",
16391639
}
16401640

1641-
store = misc.patch_pred_store(patch_output, (1.0, 1.0))
1641+
store = misc.dict_to_store(patch_output, (1.0, 1.0))
16421642

16431643
# Check that its an SQLiteStore containing the expected annotations
16441644
assert isinstance(store, SQLiteStore)
@@ -1651,7 +1651,7 @@ def test_patch_pred_store() -> None:
16511651
patch_output.pop("coordinates")
16521652
# check correct error is raised if coordinates are missing
16531653
with pytest.raises(ValueError, match="coordinates"):
1654-
misc.patch_pred_store(patch_output, (1.0, 1.0))
1654+
misc.dict_to_store(patch_output, (1.0, 1.0))
16551655

16561656

16571657
def test_patch_pred_store_cdict() -> None:
@@ -1665,7 +1665,7 @@ def test_patch_pred_store_cdict() -> None:
16651665
"other": "other",
16661666
}
16671667
class_dict = {0: "class0", 1: "class1"}
1668-
store = misc.patch_pred_store(patch_output, (1.0, 1.0), class_dict=class_dict)
1668+
store = misc.dict_to_store(patch_output, (1.0, 1.0), class_dict=class_dict)
16691669

16701670
# Check that its an SQLiteStore containing the expected annotations
16711671
assert isinstance(store, SQLiteStore)
@@ -1686,10 +1686,113 @@ def test_patch_pred_store_sf() -> None:
16861686
"probabilities": [[0.1, 0.9], [0.9, 0.1], [0.4, 0.6]],
16871687
"labels": [1, 0, 1],
16881688
}
1689-
store = misc.patch_pred_store(patch_output, (2.0, 2.0))
1689+
store = misc.dict_to_store(patch_output, (2.0, 2.0))
16901690

16911691
# Check that its an SQLiteStore containing the expected annotations
16921692
assert isinstance(store, SQLiteStore)
16931693
assert len(store) == 3
16941694
for annotation in store.values():
16951695
assert annotation.geometry.area == 4
1696+
1697+
1698+
def test_patch_pred_store_zarr(tmp_path: pytest.TempPathFactory) -> None:
1699+
"""Test patch_pred_store_zarr."""
1700+
# Define a mock patch_output
1701+
patch_output = {
1702+
"predictions": [1, 0, 1],
1703+
"coordinates": [(0, 0, 1, 1), (1, 1, 2, 2), (2, 2, 3, 3)],
1704+
"probabilities": [[0.1, 0.9], [0.9, 0.1], [0.4, 0.6]],
1705+
"labels": [1, 0, 1],
1706+
}
1707+
1708+
save_path = tmp_path / "patch_output" / "output.zarr"
1709+
1710+
store_path = misc.dict_to_zarr(patch_output, save_path=save_path)
1711+
1712+
print("Zarr path: ", store_path)
1713+
assert Path.exists(store_path), "Zarr output file does not exist"
1714+
1715+
1716+
def test_patch_pred_store_zarr_ext(tmp_path: pytest.TempPathFactory) -> None:
1717+
"""Test patch_pred_store_zarr and ensures the output file extension is `.zarr`."""
1718+
# Define a mock patch_output
1719+
patch_output = {
1720+
"predictions": [1, 0, 1],
1721+
"coordinates": [(0, 0, 1, 1), (1, 1, 2, 2), (2, 2, 3, 3)],
1722+
"probabilities": [[0.1, 0.9], [0.9, 0.1], [0.4, 0.6]],
1723+
"labels": [1, 0, 1],
1724+
}
1725+
1726+
# sends the path of a jpeg source image, expects .zarr file in the same directory
1727+
save_path = tmp_path / "patch_output" / "patch.jpeg"
1728+
1729+
store_path = misc.dict_to_zarr(patch_output, save_path=save_path)
1730+
1731+
print("Zarr path: ", store_path)
1732+
assert Path.exists(store_path), "Zarr output file does not exist"
1733+
1734+
1735+
def test_patch_pred_store_persist(tmp_path: pytest.TempPathFactory) -> None:
1736+
"""Test patch_pred_store. and persists store output to a .db file."""
1737+
# Define a mock patch_output
1738+
patch_output = {
1739+
"predictions": [1, 0, 1],
1740+
"coordinates": [(0, 0, 1, 1), (1, 1, 2, 2), (2, 2, 3, 3)],
1741+
"probabilities": [[0.1, 0.9], [0.9, 0.1], [0.4, 0.6]],
1742+
"labels": [1, 0, 1],
1743+
}
1744+
save_path = tmp_path / "patch_output" / "output.db"
1745+
1746+
store_path = misc.dict_to_store(patch_output, (1.0, 1.0), save_path=save_path)
1747+
1748+
print("Annotation store path: ", store_path)
1749+
assert Path.exists(store_path), "Annotation Store output file does not exist"
1750+
1751+
store = SQLiteStore(store_path)
1752+
1753+
# Check that its an SQLiteStore containing the expected annotations
1754+
assert isinstance(store, SQLiteStore)
1755+
assert len(store) == 3
1756+
for annotation in store.values():
1757+
assert annotation.geometry.area == 1
1758+
assert annotation.properties["type"] in [0, 1]
1759+
assert "other" not in annotation.properties
1760+
1761+
patch_output.pop("coordinates")
1762+
# check correct error is raised if coordinates are missing
1763+
with pytest.raises(ValueError, match="coordinates"):
1764+
misc.dict_to_store(patch_output, (1.0, 1.0))
1765+
1766+
1767+
def test_patch_pred_store_persist_ext(tmp_path: pytest.TempPathFactory) -> None:
1768+
"""Test patch_pred_store and ensures the output file extension is `.db`."""
1769+
# Define a mock patch_output
1770+
patch_output = {
1771+
"predictions": [1, 0, 1],
1772+
"coordinates": [(0, 0, 1, 1), (1, 1, 2, 2), (2, 2, 3, 3)],
1773+
"probabilities": [[0.1, 0.9], [0.9, 0.1], [0.4, 0.6]],
1774+
"labels": [1, 0, 1],
1775+
}
1776+
1777+
# sends the path of a jpeg source image, expects .db file in the same directory
1778+
save_path = tmp_path / "patch_output" / "output.jpeg"
1779+
1780+
store_path = misc.dict_to_store(patch_output, (1.0, 1.0), save_path=save_path)
1781+
1782+
print("Annotation store path: ", store_path)
1783+
assert Path.exists(store_path), "Annotation Store output file does not exist"
1784+
1785+
store = SQLiteStore(store_path)
1786+
1787+
# Check that its an SQLiteStore containing the expected annotations
1788+
assert isinstance(store, SQLiteStore)
1789+
assert len(store) == 3
1790+
for annotation in store.values():
1791+
assert annotation.geometry.area == 1
1792+
assert annotation.properties["type"] in [0, 1]
1793+
assert "other" not in annotation.properties
1794+
1795+
patch_output.pop("coordinates")
1796+
# check correct error is raised if coordinates are missing
1797+
with pytest.raises(ValueError, match="coordinates"):
1798+
misc.dict_to_store(patch_output, (1.0, 1.0))

tiatoolbox/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
if sys.version_info >= (3, 9): # pragma: no cover
1010
import importlib.resources as importlib_resources
1111
else: # pragma: no cover
12-
import importlib_resources # To support Python 3.8
12+
# To support Python 3.8
13+
import importlib_resources # type: ignore[import-not-found]
1314

1415
import yaml
1516

1617
if TYPE_CHECKING: # pragma: no cover
1718
from logging import LogRecord
19+
from types import ModuleType
1820

1921
__author__ = """TIA Centre"""
2022
__email__ = "[email protected]"
@@ -71,6 +73,7 @@ def filter(self: DuplicateFilter, record: LogRecord) -> bool:
7173

7274

7375
# runtime context parameters
76+
rcParam: dict[str, str | Path | dict] # noqa: N816
7477
rcParam = { # noqa: N816
7578
"TIATOOLBOX_HOME": Path.home() / ".tiatoolbox",
7679
}
@@ -88,6 +91,7 @@ def read_registry_files(path_to_registry: str | Path) -> dict | str:
8891
8992
9093
"""
94+
path_to_registry = str(path_to_registry) # To pass tests with Python 3.8
9195
pretrained_files_registry_path = importlib_resources.as_file(
9296
importlib_resources.files("tiatoolbox") / path_to_registry,
9397
)
@@ -101,8 +105,10 @@ def read_registry_files(path_to_registry: str | Path) -> dict | str:
101105
rcParam["pretrained_model_info"] = read_registry_files("data/pretrained_model.yaml")
102106

103107

104-
def _lazy_import(name: str, module_location: Path) -> sys.modules:
108+
def _lazy_import(name: str, module_location: Path) -> ModuleType:
105109
spec = importlib.util.spec_from_file_location(name, module_location)
110+
if spec is None or spec.loader is None:
111+
raise ModuleNotFoundError(name=name, path=str(module_location))
106112
loader = importlib.util.LazyLoader(spec.loader)
107113
spec.loader = loader
108114
module = importlib.util.module_from_spec(spec)

tiatoolbox/utils/exceptions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Custom Errors and Exceptions for TIAToolbox."""
2+
from __future__ import annotations
23

34

45
class FileNotSupportedError(Exception):
@@ -10,7 +11,7 @@ class FileNotSupportedError(Exception):
1011
"""
1112

1213
def __init__(
13-
self: Exception,
14+
self: FileNotSupportedError,
1415
message: str = "File format is not supported",
1516
) -> None:
1617
"""Initialize :class:`FileNotSupportedError`."""
@@ -25,6 +26,9 @@ class MethodNotSupportedError(Exception):
2526
2627
"""
2728

28-
def __init__(self: Exception, message: str = "Method is not supported") -> None:
29+
def __init__(
30+
self: MethodNotSupportedError,
31+
message: str = "Method is not supported",
32+
) -> None:
2933
"""Initialize :class:`MethodNotSupportedError`."""
3034
super().__init__(message)

0 commit comments

Comments
 (0)