Repository: ipython/ipython

Issue:
Moving Constructor definition to the top like a Function definition
I would like to know if there is any specific reason for displaying the constructor definition at the bottom unlike a normal function definition. Currently it is less user-friendly to check up the default values of the arguments in the constructor. For example : https://github.com/scikit-image/scikit-image/issues/885#issuecomment-34863149



Discussion:
Hum... even wondering if this is not a regression...
I'll mark as 2.0, but not sure it will make it through.

I don't think this is a regression - I think we've always printed the class docstring above the constructor info. We have to guess what is most likely to be useful to the user, and it's not clear that putting constructor info above the class docstring is better.

Oh, I read it was the constructor signature. 

Maybe we can peek at which one have the less lines. If constructor have less lines, it is probably just info on the signature right ?

It is constructor signature in the example shown, but we group constructor info - docstring, signature, etc. - together.


Resolving patch:
diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py
--- a/IPython/core/oinspect.py
+++ b/IPython/core/oinspect.py
@@ -442,17 +442,17 @@ def pdoc(self,obj,oname='',formatter = None):
         if formatter:
             ds = formatter(ds)
         if ds:
-            lines.append(head("Class Docstring:"))
+            lines.append(head("Class docstring:"))
             lines.append(indent(ds))
         if inspect.isclass(obj) and hasattr(obj, '__init__'):
             init_ds = getdoc(obj.__init__)
             if init_ds is not None:
-                lines.append(head("Constructor Docstring:"))
+                lines.append(head("Init docstring:"))
                 lines.append(indent(init_ds))
         elif hasattr(obj,'__call__'):
             call_ds = getdoc(obj.__call__)
             if call_ds:
-                lines.append(head("Calling Docstring:"))
+                lines.append(head("Call docstring:"))
                 lines.append(indent(call_ds))
 
         if not lines:
@@ -494,7 +494,7 @@ def pfile(self, obj, oname=''):
             # 0-offset, so we must adjust.
             page.page(self.format(openpy.read_py_file(ofile, skip_encoding_cookie=False)), lineno - 1)
 
-    def _format_fields(self, fields, title_width=12):
+    def _format_fields(self, fields, title_width=0):
         """Formats a list of fields for display.
 
         Parameters
@@ -502,10 +502,12 @@ def _format_fields(self, fields, title_width=12):
         fields : list
           A list of 2-tuples: (field_title, field_content)
         title_width : int
-          How many characters to pad titles to. Default 12.
+          How many characters to pad titles to. Default to longest title.
         """
         out = []
         header = self.__head
+        if title_width == 0:
+            title_width = max(len(title) + 2 for title, _ in fields)
         for title, content in fields:
             if len(content.splitlines()) > 1:
                 title = header(title + ":") + "\n"
@@ -518,7 +520,7 @@ def _format_fields(self, fields, title_width=12):
     pinfo_fields1 = [("Type", "type_name"),
                     ]
                     
-    pinfo_fields2 = [("String Form", "string_form"),
+    pinfo_fields2 = [("String form", "string_form"),
                     ]
 
     pinfo_fields3 = [("Length", "length"),
@@ -526,8 +528,8 @@ def _format_fields(self, fields, title_width=12):
                     ("Definition", "definition"),
                     ]
 
-    pinfo_fields_obj = [("Class Docstring", "class_docstring"),
-                        ("Constructor Docstring","init_docstring"),
+    pinfo_fields_obj = [("Class docstring", "class_docstring"),
+                        ("Init docstring", "init_docstring"),
                         ("Call def", "call_def"),
                         ("Call docstring", "call_docstring")]
 
@@ -567,6 +569,9 @@ def add_fields(fields):
             displayfields.append(("Namespace", info['namespace'].rstrip()))
 
         add_fields(self.pinfo_fields3)
+        if info['isclass'] and info['init_definition']:
+            displayfields.append(("Init definition",
+                            info['init_definition'].rstrip()))
         
         # Source or docstring, depending on detail level and whether
         # source found.
@@ -578,14 +583,9 @@ def add_fields(fields):
 
         # Constructor info for classes
         if info['isclass']:
-            if info['init_definition'] or info['init_docstring']:
-                displayfields.append(("Constructor information", ""))
-                if info['init_definition'] is not None:
-                    displayfields.append((" Definition",
-                                    info['init_definition'].rstrip()))
-                if info['init_docstring'] is not None:
-                    displayfields.append((" Docstring",
-                                        indent(info['init_docstring'])))
+            if info['init_docstring'] is not None:
+                displayfields.append(("Init docstring",
+                                    info['init_docstring']))
 
         # Info for objects:
         else:
@@ -693,11 +693,6 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
                 fname = 'Dynamically generated function. No source code available.'
             out['file'] = fname
         
-        # reconstruct the function definition and print it:
-        defln = self._getdef(obj, oname)
-        if defln:
-            out['definition'] = self.format(defln)
-
         # Docstrings only in detail 0 mode, since source contains them (we
         # avoid repetitions).  If source fails, we add them back, see below.
         if ds and detail_level == 0:
@@ -747,6 +742,11 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
 
         # and class docstring for instances:
         else:
+            # reconstruct the function definition and print it:
+            defln = self._getdef(obj, oname)
+            if defln:
+                out['definition'] = self.format(defln)
+
             # First, check whether the instance docstring is identical to the
             # class one, and print it separately if they don't coincide.  In
             # most cases they will, but it's nice to print all the info for
@@ -778,8 +778,12 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
             # Call form docstring for callable instances
             if safe_hasattr(obj, '__call__') and not is_simple_callable(obj):
                 call_def = self._getdef(obj.__call__, oname)
-                if call_def is not None:
-                    out['call_def'] = self.format(call_def)
+                if call_def:
+                    call_def = self.format(call_def)
+                    # it may never be the case that call def and definition differ,
+                    # but don't include the same signature twice
+                    if call_def != out.get('definition'):
+                        out['call_def'] = call_def
                 call_ds = getdoc(obj.__call__)
                 # Skip Python's auto-generated docstrings
                 if call_ds == _func_call_docstring:


Test patch:
diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py
--- a/IPython/core/tests/test_debugger.py
+++ b/IPython/core/tests/test_debugger.py
@@ -114,16 +114,16 @@ def test_ipdb_magics():
     ipdb> pdef example_function
      example_function(x, y, z='hello')
      ipdb> pdoc ExampleClass
-    Class Docstring:
+    Class docstring:
         Docstring for ExampleClass.
-    Constructor Docstring:
+    Init docstring:
         Docstring for ExampleClass.__init__
     ipdb> pinfo a
-    Type:       ExampleClass
-    String Form:ExampleClass()
-    Namespace:  Local...
-    Docstring:  Docstring for ExampleClass.
-    Constructor Docstring:Docstring for ExampleClass.__init__
+    Type:           ExampleClass
+    String form:    ExampleClass()
+    Namespace:      Local...
+    Docstring:      Docstring for ExampleClass.
+    Init docstring: Docstring for ExampleClass.__init__
     ipdb> continue
     
     Restore previous trace function, e.g. for coverage.py    
diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py
--- a/IPython/core/tests/test_oinspect.py
+++ b/IPython/core/tests/test_oinspect.py
@@ -242,7 +242,7 @@ def test_info():
     # case-insensitive comparison needed on some filesystems
     # e.g. Windows:
     nt.assert_equal(i['file'].lower(), fname.lower())
-    nt.assert_equal(i['definition'], 'Call(self, *a, **kw)\n')
+    nt.assert_equal(i['definition'], None)
     nt.assert_equal(i['docstring'], Call.__doc__)
     nt.assert_equal(i['source'], None)
     nt.assert_true(i['isclass'])
@@ -260,7 +260,7 @@ def test_info():
     nt.assert_equal(i['docstring'], "Modified instance docstring")
     nt.assert_equal(i['class_docstring'], Call.__doc__)
     nt.assert_equal(i['init_docstring'], Call.__init__.__doc__)
-    nt.assert_equal(i['call_docstring'], c.__call__.__doc__)
+    nt.assert_equal(i['call_docstring'], Call.__call__.__doc__)
 
     # Test old-style classes, which for example may not have an __init__ method.
     if not py3compat.PY3: