Repository: IDSIA/sacred
Pull request #454 (2019-04-14 13:01:44)

Added a small section about installing mongodb with docker locally.
This PR solves part of #420 . 

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

Coverage remained the same at 83.939% when pulling **ced0e5594e2976654421b9883831c0e39dd0542f on gabrieldemarmiesse:tutorial_docker_mongodb** into **8a1a2a006b7d3b225add965c0872937ba4baa7ef on IDSIA:master**.


Patch:
diff --git a/.travis.yml b/.travis.yml
index bafd14c..8c89c10 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,8 +6,12 @@ matrix:
   include:
   - python: "2.7"
     env: TOX_ENV=py27
-  - python: "3.4"
-    env: TOX_ENV=py34
+  - python: "3.5"
+    env: TOX_ENV=py35
+  - python: "3.6"
+    env: TOX_ENV=tensorflow-1
+  - python: "3.6"
+    env: TOX_ENV=tensorflow-2
   - python: "3.6"
     env: TOX_ENV=flake8
   - python: "3.5"
diff --git a/README.rst b/README.rst
index 57a69c3..62ce187 100644
--- a/README.rst
+++ b/README.rst
@@ -106,10 +106,12 @@ Contributing
 If you find a bug, have a feature request or want to discuss something general you are welcome to open an
 `issue <https://github.com/IDSIA/sacred/issues>`_. If you have a specific question related
 to the usage of sacred, please ask a question on StackOverflow under the
-`python-sacred tag <https://stackoverflow.com/questions/tagged/python-sacred>`_.
+`python-sacred tag <https://stackoverflow.com/questions/tagged/python-sacred>`_. We value documentation
+a lot. If you find something that should be included in the documentation please
+document it or let us know whats missing. If you are using Sacred in one of your projects and want to share
+your code with others, put your repo in the `Projects using Sacred <docs/projects_using_sacred.rst`>_ list.
 Pull requests are highly welcome!
 
-
 Frontends
 ---------
 At this point there are three frontends to the database entries created by sacred (that I'm aware of).
diff --git a/appveyor.yml b/appveyor.yml
index f0eebe4..7ce6c99 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -9,11 +9,11 @@ environment:
     - PYTHON: "C:\\Python27"
       PYTHON_VERSION: "2.7.x"
 
-    - PYTHON: "C:\\Python34"
-      PYTHON_VERSION: "3.4.x"
-
     - PYTHON: "C:\\Python35"
       PYTHON_VERSION: "3.5.x"
+
+    - PYTHON: "C:\\Python36"
+      PYTHON_VERSION: "3.6.x"
 build: false
 init:
   - "ECHO %PYTHON%"
diff --git a/dev-requirements.txt b/dev-requirements.txt
index ce9970f..a2f0295 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -9,8 +9,9 @@ MarkupSafe==0.23
 mock==2.0.0
 mongomock==3.7.0
 munch==2.0.4
-numpy==1.11.3
-pandas==0.19.2
+numpy==1.16.2
+packaging>=18.0
+pandas==0.24.2
 pbr==1.10.0
 py-cpuinfo==4.0
 pymongo==3.4.0
diff --git a/docs/examples.rst b/docs/examples.rst
index a451aa1..1a0cd21 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -76,6 +76,8 @@ if you feel more comfortable with classical Python. At its core Sacred is about
 tracking computatonal experiments, not about any particular coding style.
 
 
+.. _docker_setup:
+
 Docker Setup
 ============
 `examples/docker <https://github.com/IDSIA/sacred/tree/master/examples/docker>`_
diff --git a/docs/index.rst b/docs/index.rst
index e6bcdcf..f7cad03 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -54,6 +54,7 @@ Contents
    optional
    settings
    examples
+   projects_using_sacred
    tensorflow
    apidoc
    internals
diff --git a/docs/observers.rst b/docs/observers.rst
index 34e0540..a0d2fc5 100644
--- a/docs/observers.rst
+++ b/docs/observers.rst
@@ -79,6 +79,9 @@ This assumes you either have a local MongoDB running or have access to it over
 network without authentication.
 (See `here <http://docs.mongodb.org/manual/installation/>`_ on how to install)
 
+You can setup MongoDB easily with Docker. See the instructions
+in  :ref:`docker_setup` .
+
 Authentication
 --------------
 If you need authentication a little more work might be necessary.
diff --git a/docs/projects_using_sacred.rst b/docs/projects_using_sacred.rst
new file mode 100644
index 0000000..9e4b77f
--- /dev/null
+++ b/docs/projects_using_sacred.rst
@@ -0,0 +1,11 @@
+Projects using Sacred
+*********************
+
+This is a curated list of projects that make use of Sacred. The list can include
+code from research projects or show how to integrate sacred with another library.
+
+
+`Sacred_HyperOpt <https://github.com/yuvalatzmon/SACRED_HyperOpt_v2>`_
+======================================================================
+An example for integrating a general machine learning training script with Sacred
+and `HyperOpt <https://github.com/hyperopt/hyperopt>`_ (Distributed Asynchronous Hyperparameter Optimization).
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index ab6b317..399882e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,4 @@ py-cpuinfo>=4.0
 pytest>=3.0.5
 six>=1.10.0
 wrapt>=1.10.8
+packaging>=18.0
diff --git a/sacred/observers/mongo.py b/sacred/observers/mongo.py
index c9fadfd..e436fa8 100644
--- a/sacred/observers/mongo.py
+++ b/sacred/observers/mongo.py
@@ -52,7 +52,8 @@ def force_bson_encodeable(obj):
 
 class MongoObserver(RunObserver):
     COLLECTION_NAME_BLACKLIST = {'fs.files', 'fs.chunks', '_properties',
-                                 'system.indexes', 'seach_space'}
+                                 'system.indexes', 'search_space',
+                                 'search_spaces'}
     VERSION = 'MongoObserver-0.7.0'
 
     @staticmethod
@@ -63,8 +64,11 @@ class MongoObserver(RunObserver):
         import gridfs
 
         if client is not None:
-            assert isinstance(client, pymongo.MongoClient)
-            assert url is None, 'Cannot pass both a client and an url.'
+            if not isinstance(client, pymongo.MongoClient):
+                raise ValueError("client needs to be a pymongo.MongoClient, "
+                                 "but is {} instead".format(type(client)))
+            if url is not None:
+                raise ValueError('Cannot pass both a client and a url.')
         else:
             client = pymongo.MongoClient(url, **kwargs)
         database = client[db_name]
@@ -223,7 +227,7 @@ class MongoObserver(RunObserver):
         """
         if self.metrics is None:
             # If, for whatever reason, the metrics collection has not been set
-            # do not try to save there anything
+            # do not try to save anything there.
             return
         for key in metrics_by_name:
             query = {"run_id": self.run_entry['_id'],
diff --git a/sacred/randomness.py b/sacred/randomness.py
index 0f3f09c..118ecc2 100644
--- a/sacred/randomness.py
+++ b/sacred/randomness.py
@@ -5,7 +5,8 @@ from __future__ import division, print_function, unicode_literals
 import random
 
 import sacred.optional as opt
-from sacred.utils import module_is_in_cache, int_types
+from sacred.utils import module_is_in_cache, get_package_version, \
+    parse_version, int_types
 
 SEEDRANGE = (1, int(1e9))
 
@@ -30,7 +31,15 @@ def set_global_seed(seed):
     if opt.has_numpy:
         opt.np.random.seed(seed)
     if module_is_in_cache('tensorflow'):
-        import tensorflow as tf
+        # Ensures backward and forward compatibility with TensorFlow 1 and 2.
+        if get_package_version('tensorflow') < parse_version('1.13.1'):
+            import warnings
+            warnings.warn("Use of TensorFlow 1.12 and older is deprecated. "
+                          "Use Tensorflow 1.13 or newer instead.",
+                          DeprecationWarning)
+            import tensorflow as tf
+        else:
+            import tensorflow.compat.v1 as tf
         tf.set_random_seed(seed)
     if module_is_in_cache('torch'):
         import torch
diff --git a/sacred/stdout_capturing.py b/sacred/stdout_capturing.py
index b21cc65..2b9a987 100644
--- a/sacred/stdout_capturing.py
+++ b/sacred/stdout_capturing.py
@@ -136,7 +136,7 @@ def tee_output_fd():
             tee_stderr = subprocess.Popen(
                 ['tee', '-a', target.name], preexec_fn=os.setsid,
                 stdin=subprocess.PIPE, stdout=2)
-        except (FileNotFoundError, (OSError, AttributeError)):
+        except (FileNotFoundError, OSError, AttributeError):
             # No tee found in this operating system. Trying to use a python
             # implementation of tee. However this is slow and error-prone.
             tee_stdout = subprocess.Popen(
diff --git a/sacred/stflow/method_interception.py b/sacred/stflow/method_interception.py
index 3cc02d4..aafee38 100644
--- a/sacred/stflow/method_interception.py
+++ b/sacred/stflow/method_interception.py
@@ -1,10 +1,19 @@
 from .contextlibbackport import ContextDecorator
 from .internal import ContextMethodDecorator
 import sacred.optional as opt
+from sacred.utils import get_package_version, parse_version
 if opt.has_tensorflow:
-    import tensorflow
+    # Ensures backward and forward compatibility with TensorFlow 1 and 2.
+    if get_package_version('tensorflow') < parse_version('1.13.1'):
+        import warnings
+        warnings.warn("Use of TensorFlow 1.12 and older is deprecated. "
+                      "Use Tensorflow 1.13 or newer instead.",
+                      DeprecationWarning)
+        import tensorflow as tf
+    else:
+        import tensorflow.compat.v1 as tf
 else:
-    tensorflow = None
+    tf = None
 
 
 class LogFileWriter(ContextDecorator, ContextMethodDecorator):
@@ -71,6 +80,6 @@ class LogFileWriter(ContextDecorator, ContextMethodDecorator):
             return result
 
         ContextMethodDecorator.__init__(self,
-                                        tensorflow.summary.FileWriter,
+                                        tf.summary.FileWriter,
                                         "__init__",
                                         log_writer_decorator)
diff --git a/sacred/utils.py b/sacred/utils.py
index 2256668..0bd9515 100755
--- a/sacred/utils.py
+++ b/sacred/utils.py
@@ -4,6 +4,7 @@ from __future__ import division, print_function, unicode_literals
 
 import collections
 import contextlib
+import importlib
 import inspect
 import logging
 import os.path
@@ -14,6 +15,7 @@ import sys
 import threading
 import traceback as tb
 from functools import partial
+from packaging import version
 
 import wrapt
 
@@ -675,6 +677,17 @@ def module_is_imported(modname, scope=None):
     return False
 
 
+def parse_version(version_string):
+    """Returns a parsed version string."""
+    return version.parse(version_string)
+
+
+def get_package_version(name):
+    """Returns a parsed version string of a package."""
+    version_string = importlib.import_module(name).__version__
+    return parse_version(version_string)
+
+
 def ensure_wellformed_argv(argv):
     if argv is None:
         argv = sys.argv
diff --git a/setup.py b/setup.py
index dd9894d..4b0166a 100755
--- a/setup.py
+++ b/setup.py
@@ -10,6 +10,7 @@ Operating System :: OS Independent
 Programming Language :: Python :: 2.7
 Programming Language :: Python :: 3.4
 Programming Language :: Python :: 3.5
+Programming Language :: Python :: 3.6
 Topic :: Utilities
 Topic :: Scientific/Engineering
 Topic :: Scientific/Engineering :: Artificial Intelligence
@@ -45,6 +46,7 @@ setup(
         'wrapt>=1.0, <2.0',
         'py-cpuinfo>=4.0',
         'colorama>=0.4',
+        'packaging>=18.0'
     ],
     tests_require=[
         'mock>=0.8, <3.0',
diff --git a/tox.ini b/tox.ini
index 202f5b1..39969d2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,9 +4,10 @@
 # and then run "tox" from this directory.
 
 [tox]
-envlist = py27, py35, py36, py37, flake8
+envlist = py27, py35, py36, py37, flake8, tensorflow-1, tensorflow-2
 # py32 does not work because of the 'wrapt' dependency
 # py33 is not supported by newer versions of numpy
+# py34 is not supported by newer versions of pandas
 
 [testenv]
 deps =
@@ -15,6 +16,24 @@ commands =
     pytest \
         {posargs} # substitute with tox' positional arguments
 
+[testenv:tensorflow-1]
+basepython = python
+deps =
+    -rdev-requirements.txt
+    tensorflow==1.12.2
+commands =
+    pytest tests/test_stflow \
+        {posargs}
+
+[testenv:tensorflow-2]
+basepython = python
+deps =
+    -rdev-requirements.txt
+    tensorflow==2.0.0-alpha0
+commands =
+    pytest tests/test_stflow \
+        {posargs}
+
 [testenv:flake8]
 basepython = python
 deps =


Test patch:
diff --git a/tests/test_observers/test_mongo_observer.py b/tests/test_observers/test_mongo_observer.py
index 9500d71..54a7134 100644
--- a/tests/test_observers/test_mongo_observer.py
+++ b/tests/test_observers/test_mongo_observer.py
@@ -17,6 +17,19 @@ T1 = datetime.datetime(1999, 5, 4, 3, 2, 1)
 T2 = datetime.datetime(1999, 5, 5, 5, 5, 5)
 
 
+def test_create_should_raise_error_on_non_pymongo_client():
+    client = mongomock.MongoClient()
+    with pytest.raises(ValueError):
+        MongoObserver.create(client=client)
+
+
+def test_create_should_raise_error_on_both_client_and_url():
+    real_client = pymongo.MongoClient()
+    with pytest.raises(ValueError,
+                       match="Cannot pass both a client and a url."):
+        MongoObserver.create(client=real_client, url="mymongourl")
+
+
 @pytest.fixture
 def mongo_obs():
     db = mongomock.MongoClient().db
diff --git a/tests/test_stflow/test_internal.py b/tests/test_stflow/test_internal.py
index 0ee2c84..d83734a 100644
--- a/tests/test_stflow/test_internal.py
+++ b/tests/test_stflow/test_internal.py
@@ -2,7 +2,9 @@ from sacred.stflow.internal import ContextMethodDecorator
 
 
 def test_context_method_decorator():
-    " Ensure that ContextMethodDecorator can intercept method calls "
+    """
+    Ensure that ContextMethodDecorator can intercept method calls.
+    """
     class FooClass():
         def __init__(self, x):
             self.x = x
diff --git a/tests/test_stflow/test_method_interception.py b/tests/test_stflow/test_method_interception.py
index e269977..c701c03 100644
--- a/tests/test_stflow/test_method_interception.py
+++ b/tests/test_stflow/test_method_interception.py
@@ -3,6 +3,7 @@ import pytest
 
 from sacred import Experiment
 from sacred.stflow import LogFileWriter
+from sacred.utils import get_package_version, parse_version
 
 
 @pytest.fixture
@@ -10,25 +11,31 @@ def ex():
     return Experiment('tensorflow_tests')
 
 
-# Creates a simplified tensorflow interface if necessary
-# so tensorflow is not required during the tests
 @pytest.fixture()
 def tf():
+    """
+    Creates a simplified tensorflow interface if necessary,
+    so `tensorflow` is not required during the tests.
+    """
     from sacred.optional import has_tensorflow
     if has_tensorflow:
-        import tensorflow
-        return tensorflow
+        # Ensures backward and forward compatibility with TensorFlow 1 and 2.
+        if get_package_version('tensorflow') < parse_version('1.13.1'):
+            import tensorflow as tf
+        else:
+            import tensorflow.compat.v1 as tf
+        return tf
     else:
         # Let's define a mocked tensorflow
-        class tensorflow():
-            class summary():
-                class FileWriter():
+        class tensorflow:
+            class summary:
+                class FileWriter:
                     def __init__(self, logdir, graph):
                         self.logdir = logdir
                         self.graph = graph
                         print("Mocked FileWriter got logdir=%s, graph=%s" % (logdir, graph))
 
-            class Session():
+            class Session:
                 def __init__(self):
                     self.graph = None
 
@@ -40,12 +47,14 @@ def tf():
 
         # Set stflow to use the mock as the test
         import sacred.stflow.method_interception
-        sacred.stflow.method_interception.tensorflow = tensorflow
+        sacred.stflow.method_interception.tf = tensorflow
         return tensorflow
 
 
-# Tests whether logdir is stored into the info dictionary when creating a new FileWriter object
 def test_log_file_writer(ex, tf):
+    """
+    Tests whether logdir is stored into the info dictionary when creating a new FileWriter object.
+    """
     TEST_LOG_DIR = "/dev/null"
     TEST_LOG_DIR2 = "/tmp/sacred_test"
 
@@ -65,7 +74,9 @@ def test_log_file_writer(ex, tf):
 
 
 def test_log_summary_writer_as_context_manager(ex, tf):
-    """ Check that Tensorflow log directory is captured by LogFileWriter context manager"""
+    """
+    Check that Tensorflow log directory is captured by LogFileWriter context manager.
+    """
     TEST_LOG_DIR = "/dev/null"
     TEST_LOG_DIR2 = "/tmp/sacred_test"
 
@@ -92,8 +103,11 @@ def test_log_summary_writer_as_context_manager(ex, tf):
 
     ex.run()
 
+
 def test_log_file_writer_as_context_manager_with_exception(ex, tf):
-    """ Check that Tensorflow log directory is captured by LogFileWriter context manager"""
+    """
+    Check that Tensorflow log directory is captured by LogFileWriter context manager.
+    """
     TEST_LOG_DIR = "/tmp/sacred_test"
 
     @ex.main
@@ -115,9 +129,12 @@ def test_log_file_writer_as_context_manager_with_exception(ex, tf):
 
     ex.run()
 
-# Tests whether logdir is stored into the info dictionary when creating a new FileWriter object,
-# but this time on a method of a class
+
 def test_log_summary_writer_class(ex, tf):
+    """
+    Tests whether logdir is stored into the info dictionary when creating a new FileWriter object,
+    but this time on a method of a class.
+    """
     TEST_LOG_DIR = "/dev/null"
     TEST_LOG_DIR2 = "/tmp/sacred_test"
 
@@ -150,5 +167,6 @@ def test_log_summary_writer_class(ex, tf):
 
     ex.run()
 
+
 if __name__ == "__main__":
     test_log_file_writer(ex(), tf())
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b0f2e97..ed763bc 100755
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -11,7 +11,8 @@ from sacred.utils import (PATHCHANGE, convert_to_nested_dict,
                           recursive_update, set_by_dotted_path, get_inheritors,
                           convert_camel_case_to_snake_case,
                           apply_backspaces_and_linefeeds, module_exists,
-                          module_is_imported, module_is_in_cache, rel_path)
+                          module_is_imported, module_is_in_cache,
+                          get_package_version, parse_version, rel_path)
 
 
 def test_recursive_update():
@@ -201,6 +202,28 @@ def test_module_is_imported_uses_caller_globals_by_default():
     assert not module_is_imported('does_not_even_exist')
 
 
+def test_get_package_version():
+    package_version = get_package_version('pytest')
+    assert str(package_version) == '4.3.0'
+
+
+def test_parse_version():
+    parsed_version = parse_version('4.3.0')
+    assert str(parsed_version) == '4.3.0'
+
+
+def test_get_package_version_comparison():
+    package_version = get_package_version('pytest')
+    current_version = parse_version('4.3.0')
+    old_version = parse_version('4.2.1')
+    new_version = parse_version('4.4.1')
+    assert package_version == current_version
+    assert not package_version < current_version
+    assert not package_version > current_version
+    assert package_version <= new_version
+    assert package_version >= old_version
+
+
 def test_rel_path():
     assert rel_path('', 'foo.bar.baz') == 'foo.bar.baz'
     assert rel_path('foo', 'foo.bar.baz') == 'bar.baz'