Repository: glotzerlab/signac
Pull request #110 (2019-01-31 15:08:51)

Release/0.9.5

csadorf: Outstanding tasks:
[ ] Clean up branch history before merge.
[ ] Setup Circle-CI PyPI release step.
[ ] Bump version numbers.

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 92083eca..cbadcc3f 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 0.9.4
+current_version = 0.9.5
 commit = True
 tag = True
 message = Bump up to version {new_version}.
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 903d3814..b176d427 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -107,9 +107,50 @@ jobs:
     docker:
       - image: pypy:3
 
+  check-metadata:
+    docker:
+      - image: circleci/python:3.7
+    working_directory: ~/repo
+    steps:
+      - checkout
+      - run:
+          name: check-zenodo-metadata
+          command: |
+            python -c "import json; json.load(open('.zenodo.json'))"
+      - run:
+          name: references.bib
+          command: |
+            pip install --user -U pybtex
+            python -c "import pybtex; print(pybtex.format_from_file(open('references.bib'), style='unsrt', output_backend='text'))"
+
+  test-deploy-pypi:
+    docker:
+      - image: circleci/python:3.7
+    working_directory: ~/repo
+    steps:
+      - checkout
+      - run:
+          name: test-deploy-pypi
+          command: |
+            pip install --user -U -r .requirements-deploy.txt
+            bash .test-deploy.bash
+
+  deploy-pypi:
+    docker:
+      - image: circleci/python:3.7
+    working_directory: ~/repo
+    steps:
+      - checkout
+      - run:
+          name: deploy-pypi
+          command: |
+            pip install --user -U -r .requirements-deploy.txt
+            bash .deploy.bash
+
+
 workflows:
   version: 2
-  style-check-and-tests:
+  test:
     jobs:
       - style-check
       - test-2.7:
@@ -133,3 +174,27 @@ workflows:
       - test-pypy-3:
           requires:
             - test-3.6
+      - check-metadata:
+          filters:
+            branches:
+              only: /release\/.*/
+      - test-deploy-pypi:
+          filters:
+            branches:
+              only: /release\/.*/
+          requires:
+            - test-2.7
+            - test-3.4
+            - test-3.5
+            - test-3.6
+            - test-3.7
+            - test-pypy-2
+            - test-pypy-3
+  deploy:
+    jobs:
+      - deploy-pypi:
+          filters:
+            branches:
+              ignore: /.*/
+            tags:
+              only: /v.*/
diff --git a/.deploy.bash b/.deploy.bash
index 9852a1f7..6af5df3f 100755
--- a/.deploy.bash
+++ b/.deploy.bash
@@ -3,8 +3,6 @@
 set -e
 set -u
 
-python -m pip install -U twine wheel setuptools
-
 # PYPI_USERNAME - (Required) Username for the publisher's account on PyPI
 # PYPI_PASSWORD - (Required, Secret) Password for the publisher's account on PyPI
 
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..dcc47964
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,1 @@
+* @csadorf @glotzerlab/signac-maintainers
diff --git a/.requirements-deploy.txt b/.requirements-deploy.txt
new file mode 100644
index 00000000..88ec2714
--- /dev/null
+++ b/.requirements-deploy.txt
@@ -0,0 +1,3 @@
+twine
+wheel
+setuptools
diff --git a/README.md b/README.md
index a7037de0..88bd0c81 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 [![PyPi](https://img.shields.io/pypi/v/signac.svg)](https://pypi.org/project/signac/)
 [![Anaconda-Server Badge](https://anaconda.org/conda-forge/signac/badges/version.svg)](https://anaconda.org/conda-forge/signac)
 [![conda-forge-downloads](https://img.shields.io/conda/dn/conda-forge/signac.svg)](https://anaconda.org/conda-forge/signac)
-[![RTD](https://readthedocs.org/projects/signac/badge/?version=latest)](https://signac.readthedocs.io)
+[![RTD](https://readthedocs.org/projects/signac/badge/?version=latest)](https://docs.signac.io)
 [![License](https://img.shields.io/github/license/csadorf/signac.svg)](https://github.com/glotzerlab/signac/blob/master/LICENSE.txt)
 
 ## About
@@ -14,7 +14,7 @@ The [signac framework](http://www.signac.io) aids in the management of large and
 It provides a simple and robust data model to create a well-defined indexable storage layout for data and metadata.
 This makes it easier to operate on large data spaces, streamlines post-processing and analysis and makes data collectively accessible.
 
-**The documentation is available at: [https://signac.readthedocs.io](https://signac.readthedocs.io)**
+**The documentation is available at: [https://docs.signac.io](https://docs.signac.io)**
 
 ## Installation
 
@@ -29,7 +29,7 @@ To install **signac** *via* **pip**, execute:
 
     pip install signac
 
-**Detailed information about alternative installation methods can be found in the [documentation](https://signac.readthedocs.io/en/latest/installation.html).**
+**Detailed information about alternative installation methods can be found in the [documentation](https://docs.signac.io/en/latest/installation.html).**
 
 ## Quickstart
 
@@ -46,18 +46,16 @@ and access the project handle:
 
 ## Documentation
 
-The documentation is hosted at [https://signac.readthedocs.io](https://signac.readthedocs.io).
+The documentation is hosted at [https://docs.signac.io](https://docs.signac.io).
 
 ## Testing
 
-You can test this package either by executing
+You can test this package by executing:
 
     $ python -m unittest discover tests/
 
-within the repository root directory or with [tox](https://tox.readthedocs.io/en/latest/).
-
 ## Acknowledgment
 
 When using **signac** as part of your work towards a publication, we would really appreciate that you acknowledge **signac** appropriately.
-We have prepared examples on how to do that [here](http://signac.readthedocs.io/en/latest/acknowledge.html).
+We have prepared examples on how to do that [here](http://docs.signac.io/en/latest/acknowledge.html).
 **Thank you very much!**
diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml
deleted file mode 100644
index 4e73a9cd..00000000
--- a/bitbucket-pipelines.yml
+++ /dev/null
@@ -1,111 +0,0 @@
-# This is a sample build configuration for Python.
-# Only use spaces to indent your .yml configuration.
-# -----
-# You can specify a custom docker image from Dockerhub as your build environment.
-image: python:3.6
-
-pipelines:
-  default:
-    - step:
-        name: 'build and test (py36)'
-        script:
-          - python --version
-          - pip --version
-          - pip install -U flake8==3.7.1
-          - python -m flake8 --show-source signac/
-          - python setup.py build
-          - python -m unittest discover tests/ -v
-  branches:
-    release/*:
-      - step:
-          name: 'build and test (py36)'
-          script:
-            - python --version
-            - pip --version
-            - pip install -U flake8==3.7.1
-            - python -m flake8 --show-source signac/
-            - python setup.py build
-            - python -m unittest discover tests/ -v
-      - step:
-          name: 'build and test (py27-3x)'
-          image: themattrix/tox
-          services:
-            - mongo
-          script:
-            - python3.6 --version
-            - pip3.6 --version
-            - pip3.6 install -U coverage flake8==3.7.1 pymongo
-            - python3.6 -m coverage --version
-            - python3.6 -m flake8 --show-source signac/
-            - python3.6 setup.py build
-            - python3.6 -m signac config -l host testing
-            - tox --version
-            - tox
-            - python3.6 -m coverage report -i --include="signac*"
-      - step:
-          name: 'check zenodo metadata'
-          script:
-            - python -c "import json; json.load(open('.zenodo.json'))"
-      - step:
-          name: 'check references.bib'
-          script:
-            - pip install -U pybtex
-            - python -c "import pybtex; print(pybtex.format_from_file(open('references.bib'), style='unsrt', output_backend='text'))"
-      - step:
-          name: 'deploy-test.pypi.org'
-          image: python:3.6
-          deployment: test
-          script:
-            - python --version
-            - python -m pip --version
-            - python -m pip install -U twine wheel setuptools
-            - python -m twine --version
-            - python -m wheel version
-            - bash .test-deploy.bash
-  tags:
-   v*:
-      - step:
-          name: 'check zenodo metadata'
-          script:
-            - python -c "import json; json.load(open('.zenodo.json'))"
-      - step:
-          name: 'build and test (py36)'
-          script:
-            - python --version
-            - pip --version
-            - pip install -U flake8==3.7.1
-            - python -m flake8 --show-source signac/
-            - python setup.py build
-            - python -m unittest discover tests/ -v
-      - step:
-          name: 'build and test (py27-3x)'
-          image: themattrix/tox
-          services:
-            - mongo
-          script:
-            - python3.6 --version
-            - pip3.6 --version
-            - pip3.6 install -U coverage flake8==3.7.1 pymongo
-            - python3.6 -m coverage --version
-            - python3.6 -m flake8 --show-source signac/
-            - python3.6 setup.py build
-            - python3.6 -m signac config -l host testing
-            - tox --version
-            - tox
-            - python3.6 -m coverage report -i --include="signac*"
-      - step:
-          name: 'deploy-pypi.org'
-          image: python:3.6
-          deployment: production
-          script:
-            - python --version
-            - python -m pip --version
-            - python -m pip install -U twine wheel setuptools
-            - python -m twine --version
-            - python -m wheel version
-            - bash .deploy.bash
-
-definitions:
-  services:
-    mongo:
-      image: mongo
diff --git a/changelog.txt b/changelog.txt
index 581619b9..9c916b5f 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -17,6 +17,17 @@ Highlights
  - Adds functions for the import and export of data spaces.
  - Add functions for the management of data on the project level, as opposed to the job level.
 
+[0.9.5] -- 2019-01-31
+--------------------
+
+Fixed
++++++
+
+ - Ensure that the next() function can be called for a JobsIterator, e.g., `project.find()`.
+ - Pickling issue that occurs when a _SyncedDict (job.statepoint, job.document, etc.) contains a list.
+ - Issue with the readline module that would cause signac shell to fail on Windows operating systems.
+
+
 [0.9.4] -- 2018-10-24
 ---------------------
 
diff --git a/doc/conf.py b/doc/conf.py
index 60072c86..be77c2e1 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -81,7 +81,7 @@ author = u'Carl Simon Adorf'
 # The short X.Y version.
 version = '0.9'
 # The full version, including alpha/beta/rc tags.
-release = '0.9.4'
+release = '0.9.5'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/setup.py b/setup.py
index 814f3e34..1209e9f9 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@ except (IOError, OSError):
 
 setup(
     name='signac',
-    version='0.9.4',
+    version='0.9.5',
     packages=find_packages(),
     zip_safe=True,
 
diff --git a/signac/__init__.py b/signac/__init__.py
index e795982c..1104dd20 100644
--- a/signac/__init__.py
+++ b/signac/__init__.py
@@ -42,7 +42,7 @@ from .core.jsondict import get_buffer_size
 from .core.jsondict import get_buffer_load
 
 
-__version__ = '0.9.4'
+__version__ = '0.9.5'
 
 __all__ = ['__version__', 'contrib', 'db', 'errors', 'warnings', 'sync',
            'cite',
diff --git a/signac/__main__.py b/signac/__main__.py
index e1817ea8..3a1374be 100644
--- a/signac/__main__.py
+++ b/signac/__main__.py
@@ -11,13 +11,19 @@ import logging
 import getpass
 import difflib
 import code
-import readline
 import importlib
 from rlcompleter import Completer
 import re
 import errno
 from pprint import pprint, pformat
 
+try:
+    import readline
+except ImportError:
+    READLINE = False
+else:
+    READLINE = True
+
 from . import Project, get_project, init_project, index
 from . import __version__
 from .common import config
@@ -543,8 +549,9 @@ def _main_import_interactive(project, origin, args):
                 signac=importlib.import_module(__package__),
                 project=project, pr=project,
                 tmp_project=tmp_project)
-            readline.set_completer(Completer(local_ns).complete)
-            readline.parse_and_bind('tab: complete')
+            if READLINE:
+                readline.set_completer(Completer(local_ns).complete)
+                readline.parse_and_bind('tab: complete')
             code.interact(
                 local=local_ns,
                 banner=SHELL_BANNER_INTERACTIVE_IMPORT.format(
@@ -955,8 +962,9 @@ def main_shell(args):
             jobs=iter(jobs()), job=job,
             signac=sys.modules['signac'])
 
-        readline.set_completer(Completer(local_ns).complete)
-        readline.parse_and_bind('tab: complete')
+        if READLINE:
+            readline.set_completer(Completer(local_ns).complete)
+            readline.parse_and_bind('tab: complete')
         code.interact(
             local=local_ns,
             banner=SHELL_BANNER.format(
diff --git a/signac/contrib/project.py b/signac/contrib/project.py
index 4abd5c76..e4ae4068 100644
--- a/signac/contrib/project.py
+++ b/signac/contrib/project.py
@@ -1557,6 +1557,10 @@ class JobsCursor(object):
         self._filter = filter
         self._doc_filter = doc_filter
 
+        # This private attribute allows us to implement the deprecated
+        # next() method for this class.
+        self._next_iter = None
+
     def __len__(self):
         # Highly performance critical code path!!
         if self._filter or self._doc_filter:
@@ -1574,6 +1578,22 @@ class JobsCursor(object):
             self._project.find_job_ids(self._filter, self._doc_filter),
             )
 
+    def next(self):
+        """Return the next element.
+
+        This function is deprecated, users should use iter(..).next() instead!
+
+        .. deprecated:: 0.9.6
+        """
+        warnings.warn("Calling next() directly on a JobsCursor is deprecated!", DeprecationWarning)
+        if self._next_iter is None:
+            self._next_iter = iter(self)
+        try:
+            return self._next_iter.next()
+        except StopIteration:
+            self._next_iter = None
+            raise
+
     def groupby(self, key=None, default=None):
         """Groups jobs according to one or more statepoint parameters.
         This method can be called on any :class:`~.JobsCursor` such as
diff --git a/signac/core/synceddict.py b/signac/core/synceddict.py
index 2a516dd3..510dcb2d 100644
--- a/signac/core/synceddict.py
+++ b/signac/core/synceddict.py
@@ -57,10 +57,13 @@ class _SyncedList(list):
 
             @wraps(outer)
             def outer_wrapped_in_load_and_save(*args, **kwargs):
-                self._parent.load()
-                ret = outer(*args, **kwargs)
-                self._parent.save()
-                return ret
+                if hasattr(self, '_parent'):
+                    self._parent.load()
+                    ret = outer(*args, **kwargs)
+                    self._parent.save()
+                    return ret
+                else:
+                    return outer(*args, **kwargs)
 
             return outer_wrapped_in_load_and_save
         else:


Test patch:
diff --git a/.test-deploy.bash b/.test-deploy.bash
index a66ad883..795fa9e4 100755
--- a/.test-deploy.bash
+++ b/.test-deploy.bash
@@ -3,8 +3,6 @@
 set -e
 set -u
 
-python -m pip install -U twine wheel setuptools
-
 # PYPI_USERNAME - (Required) Username for the publisher's account on PyPI
 # PYPI_PASSWORD - (Required, Secret) Password for the publisher's account on PyPI
 
diff --git a/tests/test_project.py b/tests/test_project.py
index 8c93de10..a08a9a81 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -7,6 +7,7 @@ import uuid
 import warnings
 import logging
 import json
+import pickle
 from tarfile import TarFile
 from zipfile import ZipFile
 
@@ -243,6 +244,23 @@ class ProjectTest(BaseProjectTest):
         self.assertEqual(1, len(list(self.project.find_jobs({'a': 0}))))
         self.assertEqual(0, len(list(self.project.find_jobs({'a': 5}))))
 
+    def test_find_jobs_next(self):
+        statepoints = [{'a': i} for i in range(5)]
+        for sp in statepoints:
+            self.project.open_job(sp).init()
+        jobs = self.project.find_jobs()
+        with warnings.catch_warnings():
+            warnings.filterwarnings('ignore', category=DeprecationWarning, module='signac')
+            for i in range(2):  # run this twice
+                jobs_ = set()
+                for i in range(len(self.project)):
+                    job = jobs.next()
+                    self.assertIn(job, self.project)
+                    jobs_.add(job)
+                with self.assertRaises(StopIteration):
+                    job = jobs.next()
+                self.assertEqual(jobs_, set(self.project))
+
     def test_find_jobs_arithmetic_operators(self):
         for i in range(10):
             self.project.open_job(dict(a=i)).init()
@@ -1784,5 +1802,24 @@ class ProjectInitTest(unittest.TestCase):
             os.chdir(cwd)
 
 
+class ProjectPicklingTest(BaseProjectTest):
+
+    def test_pickle_project_empty(self):
+        blob = pickle.dumps(self.project)
+        self.assertEqual(pickle.loads(blob), self.project)
+
+    def test_pickle_project_with_jobs(self):
+        for i in range(3):
+            self.project.open_job(dict(a=i, b=dict(c=i), d=list(range(i, i+3)))).init()
+        blob = pickle.dumps(self.project)
+        self.assertEqual(pickle.loads(blob), self.project)
+
+    def test_pickle_jobs_directly(self):
+        for i in range(3):
+            self.project.open_job(dict(a=i, b=dict(c=i), d=list(range(i, i+3)))).init()
+        for job in self.project:
+            self.assertEqual(pickle.loads(pickle.dumps(job)), job)
+
+
 if __name__ == '__main__':
     unittest.main()