From 09dd44a19a89c21c4df4806cb3e485f122537ca5 Mon Sep 17 00:00:00 2001
From: Matthias Diener <mdiener@illinois.edu>
Date: Mon, 13 Mar 2023 19:00:23 -0500
Subject: [PATCH] typedump: type improvements (#174)

* typedump: type improvements

* isort

* spelling

* a few more tests

* try to tackle format strings
---
 pytools/__init__.py  | 58 ++++++++++++++++++++++++++++++++++----------
 test/test_pytools.py | 32 ++++++++++++++++++++++++
 2 files changed, 77 insertions(+), 13 deletions(-)

diff --git a/pytools/__init__.py b/pytools/__init__.py
index 89baa9f..a2b2d41 100644
--- a/pytools/__init__.py
+++ b/pytools/__init__.py
@@ -36,8 +36,8 @@ import sys
 from functools import reduce, wraps
 from sys import intern
 from typing import (
-    Any, Callable, ClassVar, Dict, Generic, Hashable, Iterable, List, Optional, Set,
-    Tuple, TypeVar, Union)
+    Any, Callable, ClassVar, Dict, Generic, Hashable, Iterable, List, Mapping,
+    Optional, Set, Tuple, Type, TypeVar, Union)
 
 
 try:
@@ -2016,7 +2016,24 @@ class StderrToStdout:
         del self.stderr_backup
 
 
-def typedump(val, max_seq=5, special_handlers=None):
+def typedump(val: Any, max_seq: int = 5,
+             special_handlers: Optional[Mapping[Type, Callable]] = None,
+             fully_qualified_name: bool = True) -> str:
+    """
+    Return a string representation of the type of *val*, recursing into
+    iterable objects.
+
+    :arg val: The object for which the type should be returned.
+    :arg max_seq: For iterable objects, the maximum number of elements to
+        include in the return string. Lower this value if you get a
+        :class:`RecursionError`.
+    :arg special_handlers: An optional mapping of specific types to special
+        handlers.
+    :arg fully_qualified_name: Return fully qualified names, that is,
+        include module names and use ``__qualname__`` instead of ``__name__``.
+
+    :returns: A string representation of the type of *val*.
+    """
     if special_handlers is None:
         special_handlers = {}
 
@@ -2027,10 +2044,26 @@ def typedump(val, max_seq=5, special_handlers=None):
     else:
         return hdlr(val)
 
+    def objname(obj: Any) -> str:
+        if type(obj).__module__ == "builtins":
+            if fully_qualified_name:
+                return type(obj).__qualname__
+            else:
+                return type(obj).__name__
+
+        if fully_qualified_name:
+            return type(obj).__module__ + "." + type(obj).__qualname__
+        else:
+            return type(obj).__name__
+
+    # Special handling for 'str' since it is also iterable
+    if isinstance(val, str):
+        return "str"
+
     try:
         len(val)
     except TypeError:
-        return type(val).__name__
+        return objname(val)
     else:
         if isinstance(val, dict):
             return "{%s}" % (
@@ -2040,17 +2073,16 @@ def typedump(val, max_seq=5, special_handlers=None):
 
         try:
             if len(val) > max_seq:
-                return "{}({},...)".format(
-                        type(val).__name__,
-                        ",".join(typedump(x, max_seq, special_handlers)
-                            for x in val[:max_seq]))
+                t = ",".join(typedump(x, max_seq, special_handlers)
+                            for x in val[:max_seq])
+                return f"{objname(val)}({t},...)"
             else:
-                return "{}({})".format(
-                        type(val).__name__,
-                        ",".join(typedump(x, max_seq, special_handlers)
-                            for x in val))
+                t = ",".join(typedump(x, max_seq, special_handlers)
+                            for x in val)
+                return f"{objname(val)}({t})"
+
         except TypeError:
-            return val.__class__.__name__
+            return objname(val)
 
 
 def invoke_editor(s, filename="edit.txt", descr="the file"):
diff --git a/test/test_pytools.py b/test/test_pytools.py
index 8398589..df857be 100644
--- a/test/test_pytools.py
+++ b/test/test_pytools.py
@@ -730,6 +730,38 @@ def test_strtobool():
     assert strtobool(None, False) is False
 
 
+def test_typedump():
+    from pytools import typedump
+    assert typedump("") == "str"
+    assert typedump("abcdefg") == "str"
+    assert typedump(5) == "int"
+
+    assert typedump((5.0, 4)) == "tuple(float,int)"
+    assert typedump([5, 4]) == "list(int,int)"
+    assert typedump({5, 4}) == "set(int,int)"
+    assert typedump(frozenset((1, 2, 3))) == "frozenset(int,int,int)"
+
+    assert typedump([5, 4, 3, 2, 1]) == "list(int,int,int,int,int)"
+    assert typedump([5, 4, 3, 2, 1, 0]) == "list(int,int,int,int,int,...)"
+    assert typedump([5, 4, 3, 2, 1, 0], max_seq=6) == "list(int,int,int,int,int,int)"
+
+    assert typedump({5: 42, 7: 43}) == "{'5': int, '7': int}"
+
+    class C:
+        class D:
+            pass
+
+    assert typedump(C()) == "test_pytools.test_typedump.<locals>.C"
+    assert typedump(C.D()) == "test_pytools.test_typedump.<locals>.C.D"
+    assert typedump(C.D(), fully_qualified_name=False) == "D"
+
+    from pytools.datatable import DataTable
+    t = DataTable(column_names=[])
+
+    assert typedump(t) == "pytools.datatable.DataTable()"
+    assert typedump(t, special_handlers={type(t): lambda x: "foo"}) == "foo"
+
+
 if __name__ == "__main__":
     if len(sys.argv) > 1:
         exec(sys.argv[1])
-- 
GitLab