Repository: glotzerlab/signac
Pull request #119 (2019-02-09 21:02:13)

Implement option to get a project within and above a root directory.
## Description

This patch implements the option to "get" a project within a root
directory or above it. That means when get_project(root, search=True),
that is identical to switching into root and calling get_project()
without any arguments.

## Motivation

The main motivation is that it is silly that we need to switch the
working directory in order to be able to search for a project. It will
also simplify the implementatino of the "get_job()" feature.

## Types of Changes
<!-- Please select all items that apply either now or after creating the pull request: -->
- [ ] Documentation update
- [ ] Bug fix
- [x] New feature
- [x] Breaking change<sup>1</sup>

This pull request is based on
- [ ] *master*, because it is a bug fix or an update to the documentation.
- [x] *develop*, because it introduces a new feature.

<sup>1</sup>The change breaks (or has the potential to break) existing functionality.

## Checklist:
<!-- Please select all items that apply either now or after creating the pull request. -->
<!-- If you are unsure about any of these items, do not hesitate to ask! -->
- [x] I am familiar with the [**Contributing Guidelines**](https://github.com/glotzerlab/signac/blob/master/CONTRIBUTING.md).
- [x] I agree with the terms of the [**Contributor Agreement**](https://github.com/glotzerlab/signac/blob/master/ContributorAgreement.md).
- [x] My name is on the [list of contributors](https://github.com/glotzerlab/signac/blob/master/contributors.txt).
- [x] My code follows the [code style guideline](https://github.com/glotzerlab/signac/blob/master/CONTRIBUTING.md#code-style) of this project.
- [x] The changes introduced by this pull request are covered by existing or newly introduced tests.

If necessary:
- [x] I have updated the API documentation as part of the package doc-strings.
- [ ] I have created a separate pull request to update the [framework documentation](https://docs.signac.io/) on [signac-docs](https://github.com/glotzerlab/signac-docs) and linked it here.
- [ ] I have updated the [changelog](https://github.com/glotzerlab/signac/blob/master/changelog.txt).


csadorf: @bdice I've implemented this feature to make the implementation of `get_job()` easier, but I have always been annoyed that "searching" a project requires a change of the working directory.
bdice: Let's wait to merge #118 until it can be refactored with this feature.

Patch:
diff --git a/signac/contrib/project.py b/signac/contrib/project.py
index c4bba111..4c4bf49a 100644
--- a/signac/contrib/project.py
+++ b/signac/contrib/project.py
@@ -1482,7 +1482,7 @@ class Project(object):
         if root is None:
             root = os.getcwd()
         try:
-            project = cls.get_project(root=root)
+            project = cls.get_project(root=root, search=False)
         except LookupError:
             fn_config = os.path.join(root, 'signac.rc')
             if make_dir:
@@ -1508,20 +1508,27 @@ class Project(object):
                         name, os.path.abspath(root)))
 
     @classmethod
-    def get_project(cls, root=None):
+    def get_project(cls, root=None, search=True):
         """Find a project configuration and return the associated project.
 
-        :param root: The project root directory.
-            If no root directory is given, the next project found
-            within or above the current working directory is returned.
+        :param root:
+            The starting point to search for a project, defaults to the
+            current working directory.
         :type root: str
+        :param search:
+            If True, search for project configurations inside and above
+            the specified root directory, otherwise only return projects
+            with a root directory identical to the specified root arugment.
+        :type search: bool
         :returns: The project handle.
-        :raises LookupError: If no project configuration can be found."""
-        config = load_config(root=root, local=root is not None)
-        if 'project' not in config:
+        :raises LookupError: If no project configuration can be found.
+        """
+        if root is None:
+            root = os.getcwd()
+        config = load_config(root=root, local=False)
+        if 'project' not in config or (not search and config['project_dir'] != root):
             raise LookupError(
-                "Unable to determine project id for path '{}'.".format(
-                    os.getcwd() if root is None else os.path.abspath(root)))
+                "Unable to determine project id for path '{}'.".format(os.path.abspath(root)))
         return cls(config=config)
 
 
@@ -1816,14 +1823,19 @@ def init_project(name, root=None, workspace=None, make_dir=True):
     return Project.init_project(name=name, root=root, workspace=workspace, make_dir=make_dir)
 
 
-def get_project(root=None):
+def get_project(root=None, search=True):
     """Find a project configuration and return the associated project.
 
-    :param root: The project root directory.
-        If no root directory is given, the next project found
-        within or above the current working directory is returned.
+    :param root:
+        The starting point to search for a project, defaults to the
+        current working directory.
     :type root: str
+    :param search:
+        If True, search for project configurations inside and above
+        the specified root directory, otherwise only return projects
+        with a root directory identical to the specified root arugment.
+    :type search: bool
     :returns: The project handle.
-    :rtype: :py:class:`~.Project`
-    :raises LookupError: If no project configuration can be found."""
-    return Project.get_project(root=root)
+    :raises LookupError: If no project configuration can be found.
+    """
+    return Project.get_project(root=root, search=search)


Test patch:
diff --git a/tests/test_project.py b/tests/test_project.py
index 11da51b5..6ef330b7 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -1827,6 +1827,20 @@ class ProjectInitTest(unittest.TestCase):
         finally:
             os.chdir(cwd)
 
+    def test_get_project_non_local(self):
+        root = self._tmp_dir.name
+        subdir = os.path.join(root, 'subdir')
+        os.mkdir(subdir)
+        project = signac.init_project(root=root, name='testproject')
+        self.assertEqual(project, project.get_project(root=root))
+        self.assertEqual(project, signac.get_project(root=root))
+        with self.assertRaises(LookupError):
+            self.assertEqual(project, project.get_project(root=subdir, search=False))
+        with self.assertRaises(LookupError):
+            self.assertEqual(project, signac.get_project(root=subdir, search=False))
+        self.assertEqual(project, project.get_project(root=subdir, search=True))
+        self.assertEqual(project, signac.get_project(root=subdir, search=True))
+
 
 class ProjectPicklingTest(BaseProjectTest):