From 20149581f03d1656eb6a5aed0d3143900f5b9beb Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 21 Oct 2024 19:03:56 +0800 Subject: [PATCH 01/11] add type annotation --- src/monty/json.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/monty/json.py b/src/monty/json.py index 67c3e0ed..7c851b7e 100644 --- a/src/monty/json.py +++ b/src/monty/json.py @@ -79,12 +79,12 @@ def _load_redirect(redirect_file): return dict(redirect_dict) -def _check_type(obj, type_str) -> bool: +def _check_type(obj: object, type_str: str | tuple[str, ...]) -> bool: """Alternative to isinstance that avoids imports. Checks whether obj is an instance of the type defined by type_str. This removes the need to explicitly import type_str. Handles subclasses like - isinstance does. E.g.:: + isinstance does. E.g.: class A: pass @@ -99,10 +99,8 @@ class B(A): assert isinstance(b, A) assert not isinstance(a, B) - type_str: str | tuple[str] - Note for future developers: the type_str is not always obvious for an - object. For example, pandas.DataFrame is actually pandas.core.frame.DataFrame. + object. For example, pandas.DataFrame is actually "pandas.core.frame.DataFrame". To find out the type_str for an object, run type(obj).mro(). This will list all the types that an object can resolve to in order of generality (all objects have the builtins.object as the last one). From b3aed1d481917cae251a0dc18ca93c82515ede78 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 21 Oct 2024 19:06:48 +0800 Subject: [PATCH 02/11] pre-commit migrate-config --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ca69d8fa..5ce97986 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: rev: v2.3.0 hooks: - id: codespell - stages: [commit, commit-msg] + stages: [pre-commit, commit-msg] exclude_types: [html] additional_dependencies: [tomli] # needed to read pyproject.toml below py3.11 From ec073766daf90df7535fa06658f12b8071da4ed8 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 21 Oct 2024 19:16:24 +0800 Subject: [PATCH 03/11] add TestCheckType --- tests/test_json.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/test_json.py b/tests/test_json.py index e59e89d5..080c9e92 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -15,6 +15,7 @@ MontyDecoder, MontyEncoder, MSONable, + _check_type, _load_redirect, jsanitize, load, @@ -1068,3 +1069,63 @@ def test_enum(self): assert d_ == {"v": "value_a"} na2 = EnumAsDict.from_dict(d_) assert na2 == na1 + + +class TestCheckType: + def test_numpy(self): + # Test NumPy array + arr = np.array([1, 2, 3]) + + assert _check_type(arr, "numpy.ndarray") + assert isinstance(arr, np.ndarray) + + # Test NumPy generic + scalar = np.float64(3.14) + + assert _check_type(scalar, "numpy.generic") + assert isinstance(scalar, np.generic) + + @pytest.mark.skipif(pd is None, reason="pandas is not installed") + def test_pandas(self): + # Test pandas DataFrame + df = pd.DataFrame({"a": [1, 2, 3]}) + + assert _check_type(df, "pandas.core.frame.DataFrame") + assert isinstance(df, pd.DataFrame) + + assert _check_type(df, "pandas.core.base.PandasObject") + assert isinstance(df, pd.core.base.PandasObject) + + # Test pandas Series + series = pd.Series([1, 2, 3]) + + assert _check_type(series, "pandas.core.series.Series") + assert isinstance(series, pd.Series) + + assert _check_type(series, "pandas.core.base.PandasObject") + assert isinstance(series, pd.core.base.PandasObject) + + @pytest.mark.skipif(torch is None, reason="torch is not installed") + def test_torch(self): + tensor = torch.tensor([1, 2, 3]) + + assert _check_type(tensor, "torch.Tensor") + assert isinstance(tensor, torch.Tensor) + + @pytest.mark.skipif(pydantic is None, reason="pydantic is not installed") + def test_pydantic(self): + class MyModel(pydantic.BaseModel): + name: str + + model_instance = MyModel(name="Alice") + + assert _check_type(model_instance, "pydantic.main.BaseModel") + assert isinstance(model_instance, pydantic.BaseModel) + + @pytest.mark.skipif(pint is None, reason="pint is not installed") + def test_pint(self): + ureg = pint.UnitRegistry() + qty = 3 * ureg.meter + + assert _check_type(qty, "pint.registry.Quantity") + assert isinstance(qty, pint.Quantity) From 32e8cc6db27c216059a945a8dfaa21e38c231345 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 21 Oct 2024 19:44:16 +0800 Subject: [PATCH 04/11] use qualname to include local scope --- src/monty/json.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/monty/json.py b/src/monty/json.py index 7c851b7e..7c8a6ad8 100644 --- a/src/monty/json.py +++ b/src/monty/json.py @@ -111,7 +111,9 @@ class B(A): mro = type(obj).mro() except TypeError: return False - return any(o.__module__ + "." + o.__name__ == ts for o in mro for ts in type_str) + return any( + o.__module__ + "." + o.__qualname__ == ts for o in mro for ts in type_str + ) class MSONable: From e0f1fd8a692573499cd3ae578fed564451e1611f Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 21 Oct 2024 19:47:36 +0800 Subject: [PATCH 05/11] add check for subclass --- tests/test_json.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_json.py b/tests/test_json.py index 080c9e92..67fa0627 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1072,6 +1072,30 @@ def test_enum(self): class TestCheckType: + def test_subclass(self): + class A: + pass + + class B(A): + pass + + a, b = A(), B() + + class_name_A = f"{type(a).__module__}.{type(a).__qualname__}" + class_name_B = f"{type(b).__module__}.{type(b).__qualname__}" + + # a is an instance of A, but not B + assert _check_type(a, class_name_A) + assert isinstance(a, A) + assert not _check_type(a, class_name_B) + assert not isinstance(a, B) + + # b is an instance of both B and A + assert _check_type(b, class_name_B) + assert isinstance(b, B) + assert _check_type(b, class_name_A) + assert isinstance(b, A) + def test_numpy(self): # Test NumPy array arr = np.array([1, 2, 3]) From 49d46b24b9684af812334a6dc5a1a8b71d9e6ed6 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 21 Oct 2024 19:54:30 +0800 Subject: [PATCH 06/11] add test for callable --- tests/test_json.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_json.py b/tests/test_json.py index 67fa0627..98d4efc4 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1096,6 +1096,33 @@ class B(A): assert _check_type(b, class_name_A) assert isinstance(b, A) + def test_callable(self): + # Test function + def my_function(): + pass + + callable_class_name = ( + f"{type(my_function).__module__}.{type(my_function).__qualname__}" + ) + + assert _check_type(my_function, callable_class_name), callable_class_name + assert isinstance(my_function, type(my_function)) + + # Test callable class + class MyCallableClass: + def __call__(self): + pass + + callable_instance = MyCallableClass() + assert callable(callable_instance) + + callable_class_instance_name = f"{type(callable_instance).__module__}.{type(callable_instance).__qualname__}" + + assert _check_type( + callable_instance, callable_class_instance_name + ), callable_class_instance_name + assert isinstance(callable_instance, MyCallableClass) + def test_numpy(self): # Test NumPy array arr = np.array([1, 2, 3]) From befffc1a1d92f1977a088f6be266d77ef488b34a Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 21 Oct 2024 20:06:09 +0800 Subject: [PATCH 07/11] rewrite mro for class --- src/monty/json.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/monty/json.py b/src/monty/json.py index 7c8a6ad8..db7000ea 100644 --- a/src/monty/json.py +++ b/src/monty/json.py @@ -16,7 +16,7 @@ from enum import Enum from hashlib import sha1 from importlib import import_module -from inspect import getfullargspec +from inspect import getfullargspec, isclass from pathlib import Path from typing import Any from uuid import UUID, uuid4 @@ -105,12 +105,13 @@ class B(A): list all the types that an object can resolve to in order of generality (all objects have the builtins.object as the last one). """ - type_str = type_str if isinstance(type_str, tuple) else (type_str,) - # I believe this try-except is only necessary for callable types - try: - mro = type(obj).mro() - except TypeError: + if isclass(obj): return False + + type_str = type_str if isinstance(type_str, tuple) else (type_str,) + + mro = type(obj).mro() + return any( o.__module__ + "." + o.__qualname__ == ts for o in mro for ts in type_str ) From 13c097abbc79642c95ce854de55e5ddf36c4f3b5 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 21 Oct 2024 20:18:40 +0800 Subject: [PATCH 08/11] add test for class check --- src/monty/json.py | 6 +++--- tests/test_json.py | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/monty/json.py b/src/monty/json.py index db7000ea..270540f8 100644 --- a/src/monty/json.py +++ b/src/monty/json.py @@ -105,6 +105,8 @@ class B(A): list all the types that an object can resolve to in order of generality (all objects have the builtins.object as the last one). """ + # This function is intended as an alternative of "isinstance", + # therefore wouldn't check class if isclass(obj): return False @@ -112,9 +114,7 @@ class B(A): mro = type(obj).mro() - return any( - o.__module__ + "." + o.__qualname__ == ts for o in mro for ts in type_str - ) + return any(f"{o.__module__}.{o.__qualname__}" == ts for o in mro for ts in type_str) class MSONable: diff --git a/tests/test_json.py b/tests/test_json.py index 98d4efc4..799da554 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1072,7 +1072,7 @@ def test_enum(self): class TestCheckType: - def test_subclass(self): + def test_check_subclass(self): class A: pass @@ -1096,6 +1096,23 @@ class B(A): assert _check_type(b, class_name_A) assert isinstance(b, A) + def test_check_class(self): + """This should not work for classes.""" + + class A: + pass + + class B(A): + pass + + class_name_A = f"{A.__module__}.{A.__qualname__}" + class_name_B = f"{B.__module__}.{B.__qualname__}" + + # Test class behavior (should return False, like isinstance does) + assert not _check_type(A, class_name_A) + assert not _check_type(B, class_name_B) + assert not _check_type(B, class_name_A) + def test_callable(self): # Test function def my_function(): From 74eac700ec2feffb10975d9c33770e900deeb85d Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Wed, 23 Oct 2024 17:11:50 +0800 Subject: [PATCH 09/11] revert unnecessary merge change --- src/monty/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/monty/json.py b/src/monty/json.py index b21c1778..d39f3957 100644 --- a/src/monty/json.py +++ b/src/monty/json.py @@ -68,7 +68,7 @@ def _load_redirect(redirect_file) -> dict: return dict(redirect_dict) -def _check_type(obj: object, type_str: str | tuple[str, ...]) -> bool: +def _check_type(obj: object, type_str: tuple[str, ...] | str) -> bool: """Alternative to isinstance that avoids imports. Checks whether obj is an instance of the type defined by type_str. This From da245a1fe0b63f8c5264d3b49fcc8e2803b3dbc5 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Thu, 24 Oct 2024 16:36:32 +0800 Subject: [PATCH 10/11] to strring --- src/monty/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/monty/json.py b/src/monty/json.py index d39f3957..944c0a1e 100644 --- a/src/monty/json.py +++ b/src/monty/json.py @@ -92,7 +92,7 @@ class B(A): object. For example, pandas.DataFrame is actually "pandas.core.frame.DataFrame". To find out the type_str for an object, run type(obj).mro(). This will list all the types that an object can resolve to in order of generality - (all objects have the builtins.object as the last one). + (all objects have the "builtins.object" as the last one). """ # This function is intended as an alternative of "isinstance", # therefore wouldn't check class From 6e86ba2fe41fdec41dd183dab63bb7aa40d06e34 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:49:58 +0000 Subject: [PATCH 11/11] pre-commit auto-fixes --- tests/test_json.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_json.py b/tests/test_json.py index e9e740d9..91eb48aa 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1082,6 +1082,7 @@ def test_enum(self): na2 = EnumAsDict.from_dict(d_) assert na2 == na1 + class TestCheckType: def test_check_subclass(self): class A: