Repository: giampaolo/psutil
Pull request #1733 (2020-04-24 10:48:55)

Backport python 3 super()
Credits: python "future" package, Ryan Kelly.


Patch:
diff --git a/psutil/_compat.py b/psutil/_compat.py
index 64a5761e..145fb71d 100644
--- a/psutil/_compat.py
+++ b/psutil/_compat.py
@@ -2,7 +2,10 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-"""Module which provides compatibility with older Python versions."""
+"""Module which provides compatibility with older Python versions.
+This is more future-compatible rather than the opposite (prefer latest
+Python 3 way of doing things).
+"""
 
 import collections
 import contextlib
@@ -10,19 +13,33 @@ import errno
 import functools
 import os
 import sys
+import types
+
+__all__ = [
+    # constants
+    "PY3",
+    # builtins
+    "long", "range", "super", "unicode", "basestring",
+    # literals
+    "u", "b",
+    # collections module
+    "lru_cache", "redirect_stderr",
+    # shutil module
+    "which", "get_terminal_size",
+    # python 3 exceptions
+    "FileNotFoundError", "PermissionError", "ProcessLookupError",
+    "InterruptedError", "ChildProcessError", "FileExistsError"]
 
-__all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b",
-           "lru_cache", "which", "get_terminal_size", "redirect_stderr",
-           "FileNotFoundError", "PermissionError", "ProcessLookupError",
-           "InterruptedError", "ChildProcessError", "FileExistsError"]
 
 PY3 = sys.version_info[0] == 3
+_SENTINEL = object()
 
 if PY3:
     long = int
     xrange = range
     unicode = str
     basestring = str
+    range = range
 
     def u(s):
         return s
@@ -31,7 +48,7 @@ if PY3:
         return s.encode("latin-1")
 else:
     long = long
-    xrange = xrange
+    range = xrange
     unicode = unicode
     basestring = basestring
 
@@ -42,6 +59,70 @@ else:
         return s
 
 
+# --- builtins
+
+
+# Python 3 super().
+# Taken from "future" package.
+# Credit: Ryan Kelly
+if PY3:
+    super = super
+else:
+    _builtin_super = super
+
+    def super(type_=_SENTINEL, type_or_obj=_SENTINEL, framedepth=1):
+        """Like Python 3 builtin super(). If called without any arguments
+        it attempts to infer them at runtime.
+        """
+        if type_ is _SENTINEL:
+            f = sys._getframe(framedepth)
+            try:
+                # Get the function's first positional argument.
+                type_or_obj = f.f_locals[f.f_code.co_varnames[0]]
+            except (IndexError, KeyError):
+                raise RuntimeError('super() used in a function with no args')
+            try:
+                # Get the MRO so we can crawl it.
+                mro = type_or_obj.__mro__
+            except (AttributeError, RuntimeError):
+                try:
+                    mro = type_or_obj.__class__.__mro__
+                except AttributeError:
+                    raise RuntimeError('super() used in a non-newstyle class')
+            for type_ in mro:
+                #  Find the class that owns the currently-executing method.
+                for meth in type_.__dict__.values():
+                    # Drill down through any wrappers to the underlying func.
+                    # This handles e.g. classmethod() and staticmethod().
+                    try:
+                        while not isinstance(meth, types.FunctionType):
+                            if isinstance(meth, property):
+                                # Calling __get__ on the property will invoke
+                                # user code which might throw exceptions or
+                                # have side effects
+                                meth = meth.fget
+                            else:
+                                try:
+                                    meth = meth.__func__
+                                except AttributeError:
+                                    meth = meth.__get__(type_or_obj, type_)
+                    except (AttributeError, TypeError):
+                        continue
+                    if meth.func_code is f.f_code:
+                        break  # found
+                else:
+                    # Not found. Move onto the next class in MRO.
+                    continue
+                break  # found
+            else:
+                raise RuntimeError('super() called outside a method')
+
+        # Dispatch to builtin super().
+        if type_or_obj is not _SENTINEL:
+            return _builtin_super(type_, type_or_obj)
+        return _builtin_super(type_)
+
+
 # --- exceptions
 
 
@@ -57,9 +138,7 @@ else:
     #     src/future/types/exceptions/pep3151.py
     import platform
 
-    _singleton = object()
-
-    def instance_checking_exception(base_exception=Exception):
+    def _instance_checking_exception(base_exception=Exception):
         def wrapped(instance_checker):
             class TemporaryClass(base_exception):
 
@@ -86,30 +165,30 @@ else:
 
         return wrapped
 
-    @instance_checking_exception(EnvironmentError)
+    @_instance_checking_exception(EnvironmentError)
     def FileNotFoundError(inst):
-        return getattr(inst, 'errno', _singleton) == errno.ENOENT
+        return getattr(inst, 'errno', _SENTINEL) == errno.ENOENT
 
-    @instance_checking_exception(EnvironmentError)
+    @_instance_checking_exception(EnvironmentError)
     def ProcessLookupError(inst):
-        return getattr(inst, 'errno', _singleton) == errno.ESRCH
+        return getattr(inst, 'errno', _SENTINEL) == errno.ESRCH
 
-    @instance_checking_exception(EnvironmentError)
+    @_instance_checking_exception(EnvironmentError)
     def PermissionError(inst):
-        return getattr(inst, 'errno', _singleton) in (
+        return getattr(inst, 'errno', _SENTINEL) in (
             errno.EACCES, errno.EPERM)
 
-    @instance_checking_exception(EnvironmentError)
+    @_instance_checking_exception(EnvironmentError)
     def InterruptedError(inst):
-        return getattr(inst, 'errno', _singleton) == errno.EINTR
+        return getattr(inst, 'errno', _SENTINEL) == errno.EINTR
 
-    @instance_checking_exception(EnvironmentError)
+    @_instance_checking_exception(EnvironmentError)
     def ChildProcessError(inst):
-        return getattr(inst, 'errno', _singleton) == errno.ECHILD
+        return getattr(inst, 'errno', _SENTINEL) == errno.ECHILD
 
-    @instance_checking_exception(EnvironmentError)
+    @_instance_checking_exception(EnvironmentError)
     def FileExistsError(inst):
-        return getattr(inst, 'errno', _singleton) == errno.EEXIST
+        return getattr(inst, 'errno', _SENTINEL) == errno.EEXIST
 
     if platform.python_implementation() != "CPython":
         try:
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index aca5fd7d..3e3caace 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -846,7 +846,7 @@ class Connections:
             # old version - let's keep it, just in case...
             # ip = ip.decode('hex')
             # return socket.inet_ntop(socket.AF_INET6,
-            #          ''.join(ip[i:i+4][::-1] for i in xrange(0, 16, 4)))
+            #          ''.join(ip[i:i+4][::-1] for i in range(0, 16, 4)))
             ip = base64.b16decode(ip)
             try:
                 # see: https://github.com/giampaolo/psutil/issues/201
diff --git a/psutil/_pswindows.py b/psutil/_pswindows.py
index 99d5d714..c1707db7 100644
--- a/psutil/_pswindows.py
+++ b/psutil/_pswindows.py
@@ -30,8 +30,8 @@ from ._common import usage_percent
 from ._compat import long
 from ._compat import lru_cache
 from ._compat import PY3
+from ._compat import range
 from ._compat import unicode
-from ._compat import xrange
 from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS
 from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS
 from ._psutil_windows import HIGH_PRIORITY_CLASS
@@ -1060,7 +1060,7 @@ class Process(object):
     @wrap_exceptions
     def cpu_affinity_get(self):
         def from_bitmask(x):
-            return [i for i in xrange(64) if (1 << i) & x]
+            return [i for i in range(64) if (1 << i) & x]
         bitmask = cext.proc_cpu_affinity_get(self.pid)
         return from_bitmask(bitmask)
 


Test patch:
diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py
index f3358806..d7dd42b2 100644
--- a/psutil/tests/__init__.py
+++ b/psutil/tests/__init__.py
@@ -41,16 +41,16 @@ from psutil import POSIX
 from psutil import SUNOS
 from psutil import WINDOWS
 from psutil._common import bytes2human
-from psutil._common import supports_ipv6
 from psutil._common import print_color
+from psutil._common import supports_ipv6
 from psutil._compat import ChildProcessError
 from psutil._compat import FileExistsError
 from psutil._compat import FileNotFoundError
 from psutil._compat import PY3
+from psutil._compat import range
 from psutil._compat import u
 from psutil._compat import unicode
 from psutil._compat import which
-from psutil._compat import xrange
 
 if sys.version_info < (2, 7):
     import unittest2 as unittest  # requires "pip install unittest2"
@@ -869,7 +869,7 @@ class TestMemoryLeak(unittest.TestCase):
         return (diff, ncalls)
 
     def _call_ntimes(self, fun, times):
-        return self._itercall(fun, xrange(times))[0]
+        return self._itercall(fun, range(times))[0]
 
     def _call_for(self, fun, secs):
         def iterator(secs):
diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py
index 312f17d9..51ac3609 100755
--- a/psutil/tests/test_contracts.py
+++ b/psutil/tests/test_contracts.py
@@ -27,6 +27,7 @@ from psutil import POSIX
 from psutil import SUNOS
 from psutil import WINDOWS
 from psutil._compat import long
+from psutil._compat import range
 from psutil.tests import create_sockets
 from psutil.tests import enum
 from psutil.tests import get_kernel_version
@@ -620,7 +621,7 @@ class TestFetchAllProcesses(unittest.TestCase):
     def cpu_affinity(self, ret, proc):
         self.assertIsInstance(ret, list)
         assert ret != [], ret
-        cpus = range(psutil.cpu_count())
+        cpus = list(range(psutil.cpu_count()))
         for n in ret:
             self.assertIsInstance(n, int)
             self.assertIn(n, cpus)
diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py
index b0b4af1b..3d37f35e 100755
--- a/psutil/tests/test_memory_leaks.py
+++ b/psutil/tests/test_memory_leaks.py
@@ -29,6 +29,7 @@ from psutil import POSIX
 from psutil import SUNOS
 from psutil import WINDOWS
 from psutil._compat import ProcessLookupError
+from psutil._compat import super
 from psutil.tests import CIRRUS
 from psutil.tests import create_sockets
 from psutil.tests import get_test_subprocess
@@ -283,7 +284,7 @@ class TestTerminatedProcessLeaks(TestProcessObjectLeaks):
 
     @classmethod
     def setUpClass(cls):
-        super(TestTerminatedProcessLeaks, cls).setUpClass()
+        super().setUpClass()
         p = get_test_subprocess()
         cls.proc = psutil.Process(p.pid)
         cls.proc.kill()
@@ -291,7 +292,7 @@ class TestTerminatedProcessLeaks(TestProcessObjectLeaks):
 
     @classmethod
     def tearDownClass(cls):
-        super(TestTerminatedProcessLeaks, cls).tearDownClass()
+        super().tearDownClass()
         reap_children()
 
     def _call(self, fun):
diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py
index ca0a0433..74a0c427 100755
--- a/psutil/tests/test_misc.py
+++ b/psutil/tests/test_misc.py
@@ -42,7 +42,6 @@ from psutil.tests import reload_module
 from psutil.tests import ROOT_DIR
 from psutil.tests import SCRIPTS_DIR
 from psutil.tests import sh
-from psutil.tests import TOX
 from psutil.tests import TRAVIS
 from psutil.tests import unittest
 import psutil
@@ -624,9 +623,7 @@ class TestWrapNumbers(unittest.TestCase):
 # ===================================================================
 
 
-@unittest.skipIf(TOX, "can't test on TOX")
-# See: https://travis-ci.org/giampaolo/psutil/jobs/295224806
-@unittest.skipIf(TRAVIS and not os.path.exists(SCRIPTS_DIR),
+@unittest.skipIf(not os.path.exists(SCRIPTS_DIR),
                  "can't locate scripts directory")
 class TestScripts(unittest.TestCase):
     """Tests for scripts in the "scripts" directory."""