Repository: theacodes/nox
Pull request #459 (2021-07-01 16:20:47)

Add header from noxfile.py docstring #454
This commit adds the functionality for using the `noxfile.py` module
docstring as a header description for the `nox -l` option. #454 

The module docstring is now an attribute in `Manifest` which is
now populated in `discover_manifest` which is in-turn passed
to `honor_list_request` which will print the docstring if it is
present, and do nothing if it is not present.

I've also added two tests which cover these conditions (existent and
non-existent module docstring) and added to an existing
parametrized test.

FollowTheProcess: By the way, sorry for the commit spam. I tried to be clever and merge in the upstream changes and got myself confused. Still fairly new to this!

Patch:
diff --git a/nox/manifest.py b/nox/manifest.py
index dbc3304..4f6912b 100644
--- a/nox/manifest.py
+++ b/nox/manifest.py
@@ -55,15 +55,21 @@ class Manifest:
         session_functions (Mapping[str, function]): The registry of discovered
             session functions.
         global_config (.nox.main.GlobalConfig): The global configuration.
+        module_docstring (Optional[str]): The user noxfile.py docstring.
+            Defaults to `None`.
     """
 
     def __init__(
-        self, session_functions: Mapping[str, "Func"], global_config: argparse.Namespace
+        self,
+        session_functions: Mapping[str, "Func"],
+        global_config: argparse.Namespace,
+        module_docstring: Optional[str] = None,
     ) -> None:
         self._all_sessions = []  # type: List[SessionRunner]
         self._queue = []  # type: List[SessionRunner]
         self._consumed = []  # type: List[SessionRunner]
         self._config = global_config  # type: argparse.Namespace
+        self.module_docstring = module_docstring  # type: Optional[str]
 
         # Create the sessions based on the provided session functions.
         for name, func in session_functions.items():
diff --git a/nox/tasks.py b/nox/tasks.py
index 60b5f9a..afb50a8 100644
--- a/nox/tasks.py
+++ b/nox/tasks.py
@@ -104,8 +104,11 @@ def discover_manifest(
     # sorted by decorator call time.
     functions = registry.get()
 
+    # Get the docstring from the noxfile
+    module_docstring = module.__doc__
+
     # Return the final dictionary of session functions.
-    return Manifest(functions, global_config)
+    return Manifest(functions, global_config, module_docstring)
 
 
 def filter_manifest(
@@ -166,7 +169,9 @@ def honor_list_request(
         return manifest
 
     # If the user just asked for a list of sessions, print that
-    # and be done.
+    # and any docstring specified in noxfile.py and be done.
+    if manifest.module_docstring:
+        print(manifest.module_docstring.strip(), end="\n\n")
 
     print("Sessions defined in {noxfile}:\n".format(noxfile=global_config.noxfile))
 
diff --git a/noxfile.py b/noxfile.py
index 6aed057..36150cd 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+
 import functools
 import os
 import platform


Test patch:
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index 9827cfc..4031724 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -200,12 +200,21 @@ def test_honor_list_request_noop():
     assert return_value is manifest
 
 
-@pytest.mark.parametrize("description", [None, "bar"])
-def test_honor_list_request(description):
+@pytest.mark.parametrize(
+    "description, module_docstring",
+    [
+        (None, None),
+        (None, "hello docstring"),
+        ("Bar", None),
+        ("Bar", "hello docstring"),
+    ],
+)
+def test_honor_list_request(description, module_docstring):
     config = _options.options.namespace(
         list_sessions=True, noxfile="noxfile.py", color=False
     )
     manifest = mock.create_autospec(Manifest)
+    manifest.module_docstring = module_docstring
     manifest.list_all_sessions.return_value = [
         (argparse.Namespace(friendly_name="foo", description=description), True)
     ]
@@ -218,6 +227,7 @@ def test_honor_list_request_skip_and_selected(capsys):
         list_sessions=True, noxfile="noxfile.py", color=False
     )
     manifest = mock.create_autospec(Manifest)
+    manifest.module_docstring = None
     manifest.list_all_sessions.return_value = [
         (argparse.Namespace(friendly_name="foo", description=None), True),
         (argparse.Namespace(friendly_name="bar", description=None), False),
@@ -231,6 +241,44 @@ def test_honor_list_request_skip_and_selected(capsys):
     assert "- bar" in out
 
 
+def test_honor_list_request_prints_docstring_if_present(capsys):
+    config = _options.options.namespace(
+        list_sessions=True, noxfile="noxfile.py", color=False
+    )
+    manifest = mock.create_autospec(Manifest)
+    manifest.module_docstring = "Hello I'm a docstring"
+    manifest.list_all_sessions.return_value = [
+        (argparse.Namespace(friendly_name="foo", description=None), True),
+        (argparse.Namespace(friendly_name="bar", description=None), False),
+    ]
+
+    return_value = tasks.honor_list_request(manifest, global_config=config)
+    assert return_value == 0
+
+    out = capsys.readouterr().out
+
+    assert "Hello I'm a docstring" in out
+
+
+def test_honor_list_request_doesnt_print_docstring_if_not_present(capsys):
+    config = _options.options.namespace(
+        list_sessions=True, noxfile="noxfile.py", color=False
+    )
+    manifest = mock.create_autospec(Manifest)
+    manifest.module_docstring = None
+    manifest.list_all_sessions.return_value = [
+        (argparse.Namespace(friendly_name="foo", description=None), True),
+        (argparse.Namespace(friendly_name="bar", description=None), False),
+    ]
+
+    return_value = tasks.honor_list_request(manifest, global_config=config)
+    assert return_value == 0
+
+    out = capsys.readouterr().out
+
+    assert "Hello I'm a docstring" not in out
+
+
 def test_verify_manifest_empty():
     config = _options.options.namespace(sessions=(), keywords=())
     manifest = Manifest({}, config)