Repository: spulec/freezegun

Issue:
Freezetime's access to every imported module's attribute triggers celery's deprecation warnings
Since https://github.com/spulec/freezegun/commit/70518d630c71a37d3c651dee7b73d347b5d75405 freezegun has been fetching every attribute from every imported module to check if it matches one of the patchable date. However, the celery package imports a number of modules that throw deprecation warnings on their access.  Celery slightly complicates the tracking down of this issue because it implements some LazyModule functionality so the deprecation warnings show when first accessed. Normally this prevents the warnings from showing to developer unless they intended to use a deprecated module, but since freezegun touches ever attribute on every module, the warning is always thrown.


Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/8538937/badge)](https://coveralls.io/builds/8538937)

Coverage decreased (-0.5%) to 95.726% when pulling **88b1f06d959015d916d116e7687c8e26ea4b2a57 on pelme:ignore-deprecation-warnings** into **11d9ead16a55fd31fb545a663a2b758049b4b40d on spulec:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/8539163/badge)](https://coveralls.io/builds/8539163)

Coverage decreased (-0.5%) to 95.726% when pulling **35b2fc1c3d7db2074cbdcf079413b2a8b0a5b100 on pelme:ignore-deprecation-warnings** into **11d9ead16a55fd31fb545a663a2b758049b4b40d on spulec:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/8539163/badge)](https://coveralls.io/builds/8539163)

Coverage decreased (-0.5%) to 95.726% when pulling **35b2fc1c3d7db2074cbdcf079413b2a8b0a5b100 on pelme:ignore-deprecation-warnings** into **11d9ead16a55fd31fb545a663a2b758049b4b40d on spulec:master**.


Patch:
diff --git a/freezegun/api.py b/freezegun/api.py
index 9f63fe9..4502e32 100644
--- a/freezegun/api.py
+++ b/freezegun/api.py
@@ -6,6 +6,7 @@ import time
 import calendar
 import unittest
 import platform
+import warnings
 
 from dateutil import parser
 from dateutil.tz import tzlocal
@@ -390,25 +391,28 @@ class _freeze_time(object):
         # Save the current loaded modules
         self.modules_at_start = set(sys.modules.keys())
 
-        for mod_name, module in list(sys.modules.items()):
-            if mod_name is None or module is None:
-                continue
-            elif mod_name.startswith(self.ignore):
-                continue
-            elif (not hasattr(module, "__name__") or module.__name__ in ('datetime', 'time')):
-                continue
-            for module_attribute in dir(module):
-                if module_attribute in real_names:
+        with warnings.catch_warnings():
+            warnings.filterwarnings('ignore')
+
+            for mod_name, module in list(sys.modules.items()):
+                if mod_name is None or module is None:
                     continue
-                try:
-                    attribute_value = getattr(module, module_attribute)
-                except (ImportError, AttributeError, TypeError):
-                    # For certain libraries, this can result in ImportError(_winreg) or AttributeError (celery)
+                elif mod_name.startswith(self.ignore):
                     continue
-                fake = fakes.get(id(attribute_value))
-                if fake:
-                    setattr(module, module_attribute, fake)
-                    add_change((module, module_attribute, attribute_value))
+                elif (not hasattr(module, "__name__") or module.__name__ in ('datetime', 'time')):
+                    continue
+                for module_attribute in dir(module):
+                    if module_attribute in real_names:
+                        continue
+                    try:
+                        attribute_value = getattr(module, module_attribute)
+                    except (ImportError, AttributeError, TypeError):
+                        # For certain libraries, this can result in ImportError(_winreg) or AttributeError (celery)
+                        continue
+                    fake = fakes.get(id(attribute_value))
+                    if fake:
+                        setattr(module, module_attribute, fake)
+                        add_change((module, module_attribute, attribute_value))
 
         datetime.datetime.times_to_freeze.append(time_to_freeze)
         datetime.datetime.tz_offsets.append(self.tz_offset)
@@ -436,26 +440,29 @@ class _freeze_time(object):
             # Restore modules loaded after start()
             modules_to_restore = set(sys.modules.keys()) - self.modules_at_start
             self.modules_at_start = set()
-            for mod_name in modules_to_restore:
-                module = sys.modules.get(mod_name, None)
-                if mod_name is None or module is None:
-                    continue
-                elif mod_name.startswith(self.ignore):
-                    continue
-                elif (not hasattr(module, "__name__") or module.__name__ in ('datetime', 'time')):
-                    continue
-                for module_attribute in dir(module):
-                    if module_attribute in self.fake_names:
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore')
+                for mod_name in modules_to_restore:
+                    module = sys.modules.get(mod_name, None)
+                    if mod_name is None or module is None:
                         continue
-                    try:
-                        attribute_value = getattr(module, module_attribute)
-                    except (ImportError, AttributeError, TypeError):
-                        # For certain libraries, this can result in ImportError(_winreg) or AttributeError (celery)
+                    elif mod_name.startswith(self.ignore):
                         continue
-
-                    real = self.reals.get(id(attribute_value))
-                    if real:
-                        setattr(module, module_attribute, real)
+                    elif (not hasattr(module, "__name__") or module.__name__ in ('datetime', 'time')):
+                        continue
+                    for module_attribute in dir(module):
+
+                        if module_attribute in self.fake_names:
+                            continue
+                        try:
+                            attribute_value = getattr(module, module_attribute)
+                        except (ImportError, AttributeError, TypeError):
+                            # For certain libraries, this can result in ImportError(_winreg) or AttributeError (celery)
+                            continue
+
+                        real = self.reals.get(id(attribute_value))
+                        if real:
+                            setattr(module, module_attribute, real)
 
         time.time = time.time.previous_time_function
         time.gmtime = time.gmtime.previous_gmtime_function


Test patch:
diff --git a/tests/test_warnings.py b/tests/test_warnings.py
new file mode 100644
index 0000000..7747081
--- /dev/null
+++ b/tests/test_warnings.py
@@ -0,0 +1,84 @@
+import contextlib
+import datetime
+import sys
+import types
+import warnings
+
+from freezegun import freeze_time
+
+
+class ModuleWithWarning(object):
+    """
+    A module that triggers warnings on attribute access.
+
+    This does not happen with regular modules, there has to be a bit of lazy
+    module magic going on in order for this to happen.
+
+    Examples of modules that uses this pattern in real projects can be found at:
+
+    py.code - the compiler package import causes a warning to be emitted:
+    https://github.com/pytest-dev/py/blob/67987e26aadddbbe7d1ec76c16ea9be346ae9811/py/__init__.py
+    https://github.com/pytest-dev/py/blob/67987e26aadddbbe7d1ec76c16ea9be346ae9811/py/_code/_assertionold.py#L3
+
+    celery.task - the sets module is listed in __all__ in celery.task and freeze_time accesses it:
+    https://github.com/celery/celery/blob/46c92025cdec07a4a30ad44901cf66cb27346638/celery/task/__init__.py
+    https://github.com/celery/celery/blob/46c92025cdec07a4a30ad44901cf66cb27346638/celery/task/sets.py
+    """
+    __name__ = 'module_with_warning'
+    __dict__ = {}
+    warning_triggered = False
+    counter = 0
+
+    @property
+    def attribute_that_emits_a_warning(self):
+        # Use unique warning messages to avoid messages being only reported once
+        self.__class__.counter += 1
+        warnings.warn('this is test warning #{counter}'.format(counter=self.__class__.counter))
+        self.warning_triggered = True
+
+
+@contextlib.contextmanager
+def assert_module_with_emitted_warning():
+    """Install a module that triggers warnings into sys.modules and ensure the
+    warning was triggered in the with-block.  """
+    module = sys.modules['module_with_warning'] = ModuleWithWarning()
+
+    try:
+        yield
+    finally:
+        del sys.modules['module_with_warning']
+
+    assert module.warning_triggered
+
+
+@contextlib.contextmanager
+def assert_no_warnings():
+    """A context manager that makes sure no warnings was emitted."""
+    with warnings.catch_warnings(record=True) as caught_warnings:
+        warnings.filterwarnings('always')
+        yield
+        assert not caught_warnings
+
+
+def test_ignore_warnings_in_start():
+    """Make sure that modules being introspected in start() does not emit warnings."""
+    with assert_module_with_emitted_warning():
+        freezer = freeze_time(datetime.datetime(2016, 10, 27, 9, 56))
+
+        try:
+            with assert_no_warnings():
+                freezer.start()
+
+        finally:
+            freezer.stop()
+
+
+def test_ignore_warnings_in_stop():
+    """Make sure that modules that was loaded after start() does not trigger
+    warnings in stop()"""
+    freezer = freeze_time(datetime.datetime(2016, 10, 27, 9, 56))
+    freezer.start()
+
+    with assert_module_with_emitted_warning():
+        with assert_no_warnings():
+            freezer.stop()