Issue 9147: Add dis.code_info()
diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst
index 1d5b223..aa2c552 100644
--- a/Doc/library/dis.rst
+++ b/Doc/library/dis.rst
@@ -36,6 +36,18 @@
 The :mod:`dis` module defines the following functions and constants:
 
 
+.. function:: code_info(x=None)
+
+   Return a formatted multi-line string with detailed code object
+   information for the supplied function, method, source code string
+   or code object.
+
+   Note that the exact contents of code info strings are highly
+   implementation dependent and they may change arbitrarily across
+   Python VMs or Python releases.
+
+   .. versionadded:: 3.2
+
 .. function:: dis(x=None)
 
    Disassemble the *x* object.  *x* can denote either a module, a
diff --git a/Lib/dis.py b/Lib/dis.py
index 2f46774..b4159e0 100644
--- a/Lib/dis.py
+++ b/Lib/dis.py
@@ -19,9 +19,6 @@
        Utility function to accept strings in functions that otherwise
        expect code objects
     """
-    # ncoghlan: currently only used by dis(), but plan to add an
-    # equivalent for show_code() as well (but one that returns a
-    # string rather than printing directly to the console)
     try:
         c = compile(source, name, 'eval')
     except SyntaxError:
@@ -37,11 +34,11 @@
     if x is None:
         distb()
         return
-    if hasattr(x, '__func__'):
+    if hasattr(x, '__func__'):  # Method
         x = x.__func__
-    if hasattr(x, '__code__'):
+    if hasattr(x, '__code__'):  # Function
         x = x.__code__
-    if hasattr(x, '__dict__'):
+    if hasattr(x, '__dict__'):  # Class or module
         items = sorted(x.__dict__.items())
         for name, x1 in items:
             if isinstance(x1, _have_code):
@@ -51,11 +48,11 @@
                 except TypeError as msg:
                     print("Sorry:", msg)
                 print()
-    elif hasattr(x, 'co_code'):
+    elif hasattr(x, 'co_code'): # Code object
         disassemble(x)
-    elif isinstance(x, (bytes, bytearray)):
+    elif isinstance(x, (bytes, bytearray)): # Raw bytecode
         _disassemble_bytes(x)
-    elif isinstance(x, str):
+    elif isinstance(x, str):    # Source code
         _disassemble_str(x)
     else:
         raise TypeError("don't know how to disassemble %s objects" %
@@ -97,35 +94,54 @@
         names.append(hex(flags))
     return ", ".join(names)
 
+def code_info(x):
+    """Formatted details of methods, functions, or code."""
+    if hasattr(x, '__func__'): # Method
+        x = x.__func__
+    if hasattr(x, '__code__'): # Function
+        x = x.__code__
+    if isinstance(x, str):     # Source code
+        x = _try_compile(x, "<code_info>")
+    if hasattr(x, 'co_code'):  # Code object
+        return _format_code_info(x)
+    else:
+        raise TypeError("don't know how to disassemble %s objects" %
+                        type(x).__name__)
+
+def _format_code_info(co):
+    lines = []
+    lines.append("Name:              %s" % co.co_name)
+    lines.append("Filename:          %s" % co.co_filename)
+    lines.append("Argument count:    %s" % co.co_argcount)
+    lines.append("Kw-only arguments: %s" % co.co_kwonlyargcount)
+    lines.append("Number of locals:  %s" % co.co_nlocals)
+    lines.append("Stack size:        %s" % co.co_stacksize)
+    lines.append("Flags:             %s" % pretty_flags(co.co_flags))
+    if co.co_consts:
+        lines.append("Constants:")
+        for i_c in enumerate(co.co_consts):
+            lines.append("%4d: %r" % i_c)
+    if co.co_names:
+        lines.append("Names:")
+        for i_n in enumerate(co.co_names):
+            lines.append("%4d: %s" % i_n)
+    if co.co_varnames:
+        lines.append("Variable names:")
+        for i_n in enumerate(co.co_varnames):
+            lines.append("%4d: %s" % i_n)
+    if co.co_freevars:
+        lines.append("Free variables:")
+        for i_n in enumerate(co.co_freevars):
+            lines.append("%4d: %s" % i_n)
+    if co.co_cellvars:
+        lines.append("Cell variables:")
+        for i_n in enumerate(co.co_cellvars):
+            lines.append("%4d: %s" % i_n)
+    return "\n".join(lines)
+
 def show_code(co):
     """Show details about a code object."""
-    print("Name:             ", co.co_name)
-    print("Filename:         ", co.co_filename)
-    print("Argument count:   ", co.co_argcount)
-    print("Kw-only arguments:", co.co_kwonlyargcount)
-    print("Number of locals: ", co.co_nlocals)
-    print("Stack size:       ", co.co_stacksize)
-    print("Flags:            ", pretty_flags(co.co_flags))
-    if co.co_consts:
-        print("Constants:")
-        for i_c in enumerate(co.co_consts):
-            print("%4d: %r" % i_c)
-    if co.co_names:
-        print("Names:")
-        for i_n in enumerate(co.co_names):
-            print("%4d: %s" % i_n)
-    if co.co_varnames:
-        print("Variable names:")
-        for i_n in enumerate(co.co_varnames):
-            print("%4d: %s" % i_n)
-    if co.co_freevars:
-        print("Free variables:")
-        for i_n in enumerate(co.co_freevars):
-            print("%4d: %s" % i_n)
-    if co.co_cellvars:
-        print("Cell variables:")
-        for i_n in enumerate(co.co_cellvars):
-            print("%4d: %s" % i_n)
+    print(code_info(co))
 
 def disassemble(co, lasti=-1):
     """Disassemble a code object."""
diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py
index 7feee64..8f1783d 100644
--- a/Lib/test/test_dis.py
+++ b/Lib/test/test_dis.py
@@ -1,6 +1,6 @@
 # Minimal tests for dis module
 
-from test.support import run_unittest
+from test.support import run_unittest, captured_stdout
 import unittest
 import sys
 import dis
@@ -211,8 +211,162 @@
         self.do_disassembly_test(simple_stmt_str, dis_simple_stmt_str)
         self.do_disassembly_test(compound_stmt_str, dis_compound_stmt_str)
 
+code_info_code_info = """\
+Name:              code_info
+Filename:          {0}
+Argument count:    1
+Kw-only arguments: 0
+Number of locals:  1
+Stack size:        4
+Flags:             OPTIMIZED, NEWLOCALS, NOFREE
+Constants:
+   0: 'Formatted details of methods, functions, or code.'
+   1: '__func__'
+   2: '__code__'
+   3: '<code_info>'
+   4: 'co_code'
+   5: "don't know how to disassemble %s objects"
+   6: None
+Names:
+   0: hasattr
+   1: __func__
+   2: __code__
+   3: isinstance
+   4: str
+   5: _try_compile
+   6: _format_code_info
+   7: TypeError
+   8: type
+   9: __name__
+Variable names:
+   0: x""".format(dis.__file__)
+
+@staticmethod
+def tricky(x, y, z=True, *args, c, d, e=[], **kwds):
+    def f(c=c):
+        print(x, y, z, c, d, e, f)
+    yield x, y, z, c, d, e, f
+
+co_tricky_nested_f = tricky.__func__.__code__.co_consts[1]
+
+code_info_tricky = """\
+Name:              tricky
+Filename:          {0}
+Argument count:    3
+Kw-only arguments: 3
+Number of locals:  8
+Stack size:        7
+Flags:             OPTIMIZED, NEWLOCALS, VARARGS, VARKEYWORDS, GENERATOR
+Constants:
+   0: None
+   1: <code object f at {1}, file "{0}", line {2}>
+Variable names:
+   0: x
+   1: y
+   2: z
+   3: c
+   4: d
+   5: e
+   6: args
+   7: kwds
+Cell variables:
+   0: e
+   1: d
+   2: f
+   3: y
+   4: x
+   5: z""".format(__file__,
+                  hex(id(co_tricky_nested_f)),
+                  co_tricky_nested_f.co_firstlineno)
+
+code_info_tricky_nested_f = """\
+Name:              f
+Filename:          {0}
+Argument count:    1
+Kw-only arguments: 0
+Number of locals:  1
+Stack size:        8
+Flags:             OPTIMIZED, NEWLOCALS, NESTED
+Constants:
+   0: None
+Names:
+   0: print
+Variable names:
+   0: c
+Free variables:
+   0: e
+   1: d
+   2: f
+   3: y
+   4: x
+   5: z""".format(__file__)
+
+code_info_expr_str = """\
+Name:              <module>
+Filename:          <code_info>
+Argument count:    0
+Kw-only arguments: 0
+Number of locals:  0
+Stack size:        2
+Flags:             NOFREE
+Constants:
+   0: 1
+Names:
+   0: x"""
+
+code_info_simple_stmt_str = """\
+Name:              <module>
+Filename:          <code_info>
+Argument count:    0
+Kw-only arguments: 0
+Number of locals:  0
+Stack size:        2
+Flags:             NOFREE
+Constants:
+   0: 1
+   1: None
+Names:
+   0: x"""
+
+code_info_compound_stmt_str = """\
+Name:              <module>
+Filename:          <code_info>
+Argument count:    0
+Kw-only arguments: 0
+Number of locals:  0
+Stack size:        2
+Flags:             NOFREE
+Constants:
+   0: 0
+   1: 1
+   2: None
+Names:
+   0: x"""
+
+class CodeInfoTests(unittest.TestCase):
+    test_pairs = [
+      (dis.code_info, code_info_code_info),
+      (tricky, code_info_tricky),
+      (co_tricky_nested_f, code_info_tricky_nested_f),
+      (expr_str, code_info_expr_str),
+      (simple_stmt_str, code_info_simple_stmt_str),
+      (compound_stmt_str, code_info_compound_stmt_str),
+    ]
+
+    def test_code_info(self):
+        self.maxDiff = 1000
+        for x, expected in self.test_pairs:
+            self.assertEqual(dis.code_info(x), expected)
+
+    def test_show_code(self):
+        self.maxDiff = 1000
+        for x, expected in self.test_pairs:
+            with captured_stdout() as output:
+                dis.show_code(x)
+            self.assertEqual(output.getvalue(), expected+"\n")
+
 def test_main():
-    run_unittest(DisTests)
+    run_unittest(DisTests, CodeInfoTests)
 
 if __name__ == "__main__":
     test_main()
diff --git a/Misc/NEWS b/Misc/NEWS
index 7ec59b3..cff5864 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -90,6 +90,10 @@
 Library
 -------
 
+- Issue #9147: Added dis.code_info() which is similar to show_code()
+  but returns formatted code information in a string rather than
+  displaying on screen.
+
 - Issue #9567: functools.update_wrapper now adds a __wrapped__ attribute
   pointing to the original callable