Repository: theacodes/nox (python)

Issue:
Working directory unexpectedly set to wherever noxfile.py is.
<!-- Thank you for filing a bug! Please feel free to answer as much or as little of this template as you can. -->

**Describe the bug**
Trying to invoke nox from a different folder using the --noxfile flag sets the current working directory to where the noxfile lives.  This makes reusing a noxfile across multiple projects in a monorepo very difficult, since I can't find a good way to set the current working directory to where nox was called from.  Even passing '.' as a posarg will end up being interpreted as the directory where the noxfile lives

**Expected behavior**
Either the cwd stays as wherever nox is called from, or we have some command line arg for what to treat as the cwd.  If this feature already exists and I've missed it, or there are some other recommendations for how to use a single nox file across multiple projects, please let me know!


Pull request:
469 add initial cwd to session properties
Closes #469

Added a working directory nox was originaly invoked from to session propreties.

@DiddiLeija I started working on this issue some time ago, but then had a long break and couldn't find time to finalize it until today and now noticed that you already opened a PR for it. Hopefully we did not duplicate much work, sorry for not mentioning it earlier and please let me know if you are ok with me continuing on this one. 

Patch:
diff --git a/nox/_option_set.py b/nox/_option_set.py
index 1e81443..91dcc15 100644
--- a/nox/_option_set.py
+++ b/nox/_option_set.py
@@ -77,7 +77,7 @@ class Option:
         self,
         name: str,
         *flags: str,
-        group: OptionGroup,
+        group: Optional[OptionGroup],
         help: Optional[str] = None,
         noxfile: bool = False,
         merge_func: Optional[Callable[[Namespace, Namespace], Any]] = None,
@@ -230,9 +230,15 @@ class OptionSet:
         }
 
         for option in self.options.values():
-            if option.hidden:
+            if option.hidden is True:
                 continue
 
+            # Every option must have a group (except for hidden options)
+            if option.group is None:
+                raise ValueError(
+                    f"Option {option.name} must either have a group or be hidden."
+                )
+
             argument = groups[option.group.name].add_argument(
                 *option.flags, help=option.help, default=option.default, **option.kwargs
             )
diff --git a/nox/_options.py b/nox/_options.py
index 6251523..353ed64 100644
--- a/nox/_options.py
+++ b/nox/_options.py
@@ -474,6 +474,14 @@ options.add_options(
         hidden=True,
         finalizer_func=_color_finalizer,
     ),
+    # Stores the original working directory that Nox was invoked from,
+    # since it could be different from the Noxfile's directory.
+    _option_set.Option(
+        "invoked_from",
+        group=None,
+        hidden=True,
+        default=lambda: os.getcwd(),
+    ),
 )
 
 
diff --git a/nox/sessions.py b/nox/sessions.py
index 93a33dd..c97c7eb 100644
--- a/nox/sessions.py
+++ b/nox/sessions.py
@@ -189,6 +189,18 @@ class Session:
         """Returns True if Nox is being run in an interactive session or False otherwise."""
         return not self._runner.global_config.non_interactive and sys.stdin.isatty()
 
+    @property
+    def invoked_from(self) -> str:
+        """The directory that Nox was originally invoked from.
+
+        Since you can use the ``--noxfile / -f`` command-line
+        argument to run a Noxfile in a location different from your shell's
+        current working directory, Nox automatically changes the working directory
+        to the Noxfile's directory before running any sessions. This gives
+        you the original working directory that Nox was invoked form.
+        """
+        return self._runner.global_config.invoked_from
+
     def chdir(self, dir: Union[str, os.PathLike]) -> None:
         """Change the current working directory."""
         self.log(f"cd {dir}")
diff --git a/nox/tasks.py b/nox/tasks.py
index 7e55100..5226332 100644
--- a/nox/tasks.py
+++ b/nox/tasks.py
@@ -60,7 +60,8 @@ def load_nox_module(global_config: Namespace) -> Union[types.ModuleType, int]:
         # Move to the path where the Noxfile is.
         # This will ensure that the Noxfile's path is on sys.path, and that
         # import-time path resolutions work the way the Noxfile author would
-        # guess.
+        # guess. The original working directory (the directory that Nox was
+        # invoked from) gets stored by the .invoke_from "option" in _options.
         os.chdir(noxfile_parent_dir)
         return importlib.machinery.SourceFileLoader(
             "user_nox_module", global_config.noxfile


Test patch:
diff --git a/tests/test__option_set.py b/tests/test__option_set.py
index 202473e..3107a54 100644
--- a/tests/test__option_set.py
+++ b/tests/test__option_set.py
@@ -55,6 +55,29 @@ class TestOptionSet:
         with pytest.raises(KeyError):
             optionset.namespace(non_existant_option="meep")
 
+    def test_parser_hidden_option(self):
+        optionset = _option_set.OptionSet()
+        optionset.add_options(
+            _option_set.Option(
+                "oh_boy_i_am_hidden", hidden=True, group=None, default="meep"
+            )
+        )
+
+        parser = optionset.parser()
+        namespace = parser.parse_args([])
+        optionset._finalize_args(namespace)
+
+        assert namespace.oh_boy_i_am_hidden == "meep"
+
+    def test_parser_groupless_option(self):
+        optionset = _option_set.OptionSet()
+        optionset.add_options(
+            _option_set.Option("oh_no_i_have_no_group", group=None, default="meep")
+        )
+
+        with pytest.raises(ValueError):
+            optionset.parser()
+
     def test_session_completer(self):
         parsed_args = _options.options.namespace(sessions=(), keywords=(), posargs=[])
         all_nox_sessions = _options._session_completer(
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 4408bec..f12f02c 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -67,7 +67,10 @@ class TestSession:
             signatures=["test"],
             func=func,
             global_config=_options.options.namespace(
-                posargs=[], error_on_external_run=False, install_only=False
+                posargs=[],
+                error_on_external_run=False,
+                install_only=False,
+                invoked_from=os.getcwd(),
             ),
             manifest=mock.create_autospec(nox.manifest.Manifest),
         )
@@ -104,6 +107,7 @@ class TestSession:
         assert session.bin_paths is runner.venv.bin_paths
         assert session.bin is runner.venv.bin_paths[0]
         assert session.python is runner.func.python
+        assert session.invoked_from is runner.global_config.invoked_from
 
     def test_no_bin_paths(self):
         session, runner = self.make_session_and_runner()
@@ -155,6 +159,17 @@ class TestSession:
         assert os.getcwd() == cdto
         os.chdir(current_cwd)
 
+    def test_invoked_from(self, tmpdir):
+        cdto = str(tmpdir.join("cdbby").ensure(dir=True))
+        current_cwd = os.getcwd()
+
+        session, _ = self.make_session_and_runner()
+
+        session.chdir(cdto)
+
+        assert session.invoked_from == current_cwd
+        os.chdir(current_cwd)
+
     def test_chdir_pathlib(self, tmpdir):
         cdto = str(tmpdir.join("cdbby").ensure(dir=True))
         current_cwd = os.getcwd()