Repository: theacodes/nox (python)

Issue:
Run commands from within a specific directory
**How would this feature be useful?**

Since many commands have specific usages of the current working directory, 
running commands from within a specific directory would smooth integrating nox
is existing projects.

**Describe the solution you'd like**

```python
from pathlib import Path

import nox


@nox.session()
def test(session: nox.Session):
    session.install('pytest')
    python_package_root = Path("somewhere/deep/in/monorepo")
    session.run(sys.executable, "-m", "pytest", cwd=python_package_root)
```

**Describe alternatives you've considered**

```python
import os
from pathlib import Path

import nox


@nox.session()
def test(session: nox.Session):
    session.install('pytest')
    python_package_root = Path("somewhere/deep/in/monorepo")
    before_cwd = os.getcwd()
    os.chdir(python_package_root)
    session.run(sys.executable, "-m", "pytest")
    os.chdir(before_cwd)
```


Pull request description:
Make Session.chdir() a context manager
Closes #512. Also see the discussion here: #513.

I'm a bit unsure if the changes to the docs are sufficient. Please suggest changes as you see fit. 

Patch:
diff --git a/nox/sessions.py b/nox/sessions.py
index 043a9e2..ff4b30a 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -21,6 +21,7 @@ import re
 import sys
 import unicodedata
 import warnings
+from types import TracebackType
 from typing import (
     Any,
     Callable,
@@ -31,6 +32,7 @@ from typing import (
     Optional,
     Sequence,
     Tuple,
+    Type,
     Union,
 )
 
@@ -116,6 +118,23 @@ class Status(enum.Enum):
     SKIPPED = 2
 
 
+class _WorkingDirContext:
+    def __init__(self, dir: Union[str, os.PathLike]) -> None:
+        self._prev_working_dir = os.getcwd()
+        os.chdir(dir)
+
+    def __enter__(self) -> "_WorkingDirContext":
+        return self
+
+    def __exit__(
+        self,
+        exc_type: Optional[Type[BaseException]],
+        exc_value: Optional[BaseException],
+        traceback: Optional[TracebackType],
+    ) -> None:
+        os.chdir(self._prev_working_dir)
+
+
 class Session:
     """The Session object is passed into each user-defined session function.
 
@@ -210,10 +229,21 @@ class Session:
         """
         return self._runner.global_config.invoked_from
 
-    def chdir(self, dir: Union[str, os.PathLike]) -> None:
-        """Change the current working directory."""
+    def chdir(self, dir: Union[str, os.PathLike]) -> _WorkingDirContext:
+        """Change the current working directory.
+
+        Can be used as a context manager to automatically restore the working directory::
+
+            with session.chdir("somewhere/deep/in/monorepo"):
+                # Runs in "/somewhere/deep/in/monorepo"
+                session.run("pytest")
+
+            # Runs in original working directory
+            session.run("flake8")
+
+        """
         self.log(f"cd {dir}")
-        os.chdir(dir)
+        return _WorkingDirContext(dir)
 
     cd = chdir
     """An alias for :meth:`chdir`."""


Test patch:
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 4f31a82..262d9f3 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -164,6 +164,19 @@ class TestSession:
         assert os.getcwd() == cdto
         os.chdir(current_cwd)
 
+    def test_chdir_ctx(self, tmpdir):
+        cdto = str(tmpdir.join("cdbby").ensure(dir=True))
+        current_cwd = os.getcwd()
+
+        session, _ = self.make_session_and_runner()
+
+        with session.chdir(cdto):
+            assert os.getcwd() == cdto
+
+        assert os.getcwd() == current_cwd
+
+        os.chdir(current_cwd)
+
     def test_invoked_from(self, tmpdir):
         cdto = str(tmpdir.join("cdbby").ensure(dir=True))
         current_cwd = os.getcwd()