From 7dd754d8cccdc948ae90d7ac96ef63d30cf4a8ad Mon Sep 17 00:00:00 2001 From: Paul Madden <136389411+maddenp-noaa@users.noreply.github.com> Date: Fri, 12 Jul 2024 07:50:32 -0600 Subject: [PATCH] omnibus-2024-07-09 (#526) --- docs/environment.yml | 4 +- .../contributor_guide/documentation.rst | 13 +- recipe/meta.json | 6 +- recipe/meta.yaml | 4 +- src/setup.py | 4 +- src/uwtools/api/driver.py | 4 +- src/uwtools/config/formats/ini.py | 8 +- src/uwtools/config/formats/nml.py | 8 +- src/uwtools/drivers/driver.py | 116 +++++++----------- src/uwtools/drivers/upp.py | 4 +- src/uwtools/tests/config/formats/test_yaml.py | 9 +- .../tests/config/test_atparse_to_jinja2.py | 6 +- src/uwtools/tests/config/test_jinja2.py | 8 +- src/uwtools/tests/config/test_tools.py | 54 ++++---- src/uwtools/tests/drivers/test_driver.py | 18 ++- src/uwtools/tests/utils/test_api.py | 2 +- src/uwtools/tests/utils/test_file.py | 15 ++- 17 files changed, 130 insertions(+), 153 deletions(-) diff --git a/docs/environment.yml b/docs/environment.yml index c02aea6c3..ea7494fd7 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -2,6 +2,6 @@ name: readthedocs channels: - conda-forge dependencies: - - sphinx_rtd_theme=1.3.0 - - sphinxcontrib-bibtex=2.6.1 + - sphinx_rtd_theme=2.0.* + - sphinxcontrib-bibtex=2.6.* - tree diff --git a/docs/sections/contributor_guide/documentation.rst b/docs/sections/contributor_guide/documentation.rst index 7fd350b7b..ee82459cf 100644 --- a/docs/sections/contributor_guide/documentation.rst +++ b/docs/sections/contributor_guide/documentation.rst @@ -16,16 +16,16 @@ The ``make docs`` command will build the docs under ``docs/build/html``, after w file:///docs/build/html/index.html -Re-run ``make docs`` and refresh your browser after making and saving changes. Note that some documentation content is dynamically generated: Timestamps shown in e.g. log messages are expected and are ok to commit. +After making and saving changes, re-run ``make docs`` and refresh your browser. Note that some documentation content is dynamically generated: Timestamps shown in e.g. log messages are expected and are ok to commit. -If, at some point, you remove and recreate the conda development environment underlying your development shell, you will need to rerun the ``source install-deps`` command in the new environment/shell. Until then, the installed doc packages will persist and support docs generation. +If, at some point, you remove and recreate the conda development environment underlying your development shell, you will need to re-run the ``source install-deps`` command in the new environment/shell. Until then, the installed doc packages will persist and support docs generation. Viewing Online Documentation ---------------------------- Online documentation generation and hosting for ``uwtools`` is provided by :rtd:`Read the Docs<>`. The green *View Docs* button near the upper right of that page links to the official docs for the project. When viewing the documentation, the version selector at the bottom of the navigation column on the left can be used to switch between the latest development code (``main``), the latest released version (``stable``), and any previously released version. -Docs are also built and temporarily published when Pull Requests (PRs) targeting the ``main`` branch are opened. Visit the :rtd:`Builds page` to see recent builds, including those made for PRs. Click a PR-related build marked *Passed*, then the small *View docs* link (**not** the large green *View Docs* button) to see the docs built specifically for that PR. If your PR includes documentation updates, it may be helpful to include the URL of this build in your PR's description so that reviewers can see the rendered HTML docs and not just the modified ``.rst`` files. Note that if commits are pushed to the PR's source branch, Read the Docs will rebuild the PR docs. See the checks section near the bottom of a PR for current status, and for another link to the PR docs via the *Details* link. +Docs are also built and temporarily published when Pull Requests (PRs) targeting the ``main`` branch are opened. Visit the :rtd:`Builds page` to see recent builds, including those made for PRs. Click a PR-related build marked *Passed*, then the small *View docs* link (**not** the large green *View Docs* button) to see the docs built specifically for that PR. See the ``docs/readthedocs.org:uwtools`` item in the checks section near the bottom of the PR for current status of the PR docs build, and click the *Details* link to its right to preview the docs when they are available. Note that if commits are pushed to the PR's source branch, Read the Docs will rebuild the PR docs. Documentation Guidelines ------------------------ @@ -37,14 +37,13 @@ Please follow these guidelines when contributing to the documentation: * If the link-check portion of ``make docs`` reports that a URL is ``permanently`` redirected, update the link in the docs to use the new URL. Non-permanent redirects can be left as-is. * Do not manually wrap lines in the ``.rst`` files. Insert newlines only as needed to achieve correctly formatted HTML, and let HTML wrap long lines and/or provide a scrollbar. * Use one blank line between documentation elements (headers, paragraphs, code blocks, etc.) unless additional lines are necessary to achieve correctly formatted HTML. -* Remove all trailing whitespace. +* Remove all trailing whitespace, except where inserted by dynamic content generation -- don't fight the tooling. * In general, avoid pronouns like "we" and "you". (Using "we" may be appropriate when synonymous with "The UW Team", "The UFS Community", etc., when the context is clear.) Prefer direct, factual statements about what the code does, requires, etc. * Use the `Oxford Comma `_. -* The synopsis information printed by ``uw [mode [action]] --help`` is automatically wrapped and indented based on current terminal size. For visual consistency, please set your terminal width to 100 columns when running such commands to produce output to copy into the docs. -* Follow the :rst:`RST Sections` guidelines, underlining section headings with = characters, subsections with - characters, and subsubsections with ^ characters. If a further level of refinement is needed, use " to underline paragraph headers. +* Follow the :rst:`RST Sections` guidelines, underlining section headings with ``=`` characters, subsections with ``-`` characters, and subsubsections with ``^`` characters. If a further level of refinement is needed, indented and/or bulleted lists, as subsections marked with ``"`` are nearly indistinguishable from those marked with ``^``. * In [[sub]sub]section titles, capitalize all "principal" words. In practice this usually means all words but articles (a, an, the), logicals (and, etc.), and prepositions (for, of, etc.). Always fully capitalize acronyms (e.g., YAML). * Never capitalize proper names when their owners do not (e.g., write `"pandas" `_, not "Pandas", even at the start of a sentence) or when referring to a software artifact (e.g., write ``numpy`` when referring to the library, and "NumPy" when referring to the project). -* When referring to YAML constructs, `block` refers to an entry whose values is a nested collection of key/value pairs, while `entry` is a single key/value pair. +* When referring to YAML constructs, `block` refers to an entry whose value is a nested collection of key/value pairs, while `entry` refers to a single key/value pair. * When using the ``.. code-block::`` directive, align the actual code with the word ``code``. Also, when ``.. code-block::`` directives appear in bulleted or numberd lists, align them with the text following the space to the right of the bullet/number. For example: .. code-block:: text diff --git a/recipe/meta.json b/recipe/meta.json index 7e5c59cd3..b1152a8c9 100644 --- a/recipe/meta.json +++ b/recipe/meta.json @@ -11,7 +11,7 @@ "isort =5.13.*", "jinja2 =3.1.*", "jq =1.7.*", - "jsonschema =4.22.*", + "jsonschema =4.23.*", "lxml =5.2.*", "make >=3.8", "mypy =1.10.*", @@ -19,7 +19,7 @@ "pylint =3.2.*", "pytest =8.2.*", "pytest-cov =5.0.*", - "pytest-xdist =3.5.*", + "pytest-xdist =3.6.*", "python >=3.9,<3.13", "pyyaml =6.0.*" ], @@ -27,7 +27,7 @@ "f90nml =1.4.*", "iotaa =0.8.*", "jinja2 =3.1.*", - "jsonschema =4.22.*", + "jsonschema =4.23.*", "lxml =5.2.*", "python >=3.9,<3.13", "pyyaml =6.0.*" diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 0f3aed707..f25b00ca9 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -16,7 +16,7 @@ requirements: - f90nml 1.4.* - iotaa 0.8.* - jinja2 3.1.* - - jsonschema 4.22.* + - jsonschema 4.23.* - lxml 5.2.* - python >=3.9,<3.13 - pyyaml 6.0.* @@ -31,6 +31,6 @@ test: - pylint 3.2.* - pytest 8.2.* - pytest-cov 5.0.* - - pytest-xdist 3.5.* + - pytest-xdist 3.6.* about: license: LGPL diff --git a/src/setup.py b/src/setup.py index c4b5698f5..18ea35b5c 100644 --- a/src/setup.py +++ b/src/setup.py @@ -36,9 +36,9 @@ "version": meta["version"], } -# Define dependency packages for non-conda installs. +# Define dependency packages for non-devshell installs. -if not os.environ.get("CONDA_PREFIX"): +if not os.environ.get("CONDEV_SHELL"): kwargs["install_requires"] = [ pkg.replace(" =", "==") for pkg in meta["packages"]["run"] diff --git a/src/uwtools/api/driver.py b/src/uwtools/api/driver.py index f724ebc7e..43f2218ca 100644 --- a/src/uwtools/api/driver.py +++ b/src/uwtools/api/driver.py @@ -8,11 +8,11 @@ _CLASSNAMES = [ "Assets", "AssetsCycleBased", - "AssetsCycleAndLeadtimeBased", + "AssetsCycleLeadtimeBased", "AssetsTimeInvariant", "Driver", "DriverCycleBased", - "DriverCycleAndLeadtimeBased", + "DriverCycleLeadtimeBased", "DriverTimeInvariant", ] diff --git a/src/uwtools/config/formats/ini.py b/src/uwtools/config/formats/ini.py index ce9861bf4..87ed85a9e 100644 --- a/src/uwtools/config/formats/ini.py +++ b/src/uwtools/config/formats/ini.py @@ -40,12 +40,10 @@ def _dict_to_str(cls, cfg: dict) -> str: config_check_depths_dump(config_obj=cfg, target_format=FORMAT.ini) parser = configparser.ConfigParser() - sio = StringIO() parser.read_dict(cfg) - parser.write(sio) - s = sio.getvalue().strip() - sio.close() - return s + with StringIO() as sio: + parser.write(sio) + return sio.getvalue().strip() def _load(self, config_file: Optional[Path]) -> dict: """ diff --git a/src/uwtools/config/formats/nml.py b/src/uwtools/config/formats/nml.py index e8fbbbef9..a60b9b78d 100644 --- a/src/uwtools/config/formats/nml.py +++ b/src/uwtools/config/formats/nml.py @@ -43,11 +43,9 @@ def to_od(d): config_check_depths_dump(config_obj=cfg, target_format=FORMAT.nml) nml: Namelist = Namelist(to_od(cfg)) if not isinstance(cfg, Namelist) else cfg - sio = StringIO() - nml.write(sio, sort=False) - s = sio.getvalue() - sio.close() - return s.strip() + with StringIO() as sio: + nml.write(sio, sort=False) + return sio.getvalue().strip() def _load(self, config_file: Optional[Path]) -> dict: """ diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py index cdf5bf00d..48fd01e57 100644 --- a/src/uwtools/drivers/driver.py +++ b/src/uwtools/drivers/driver.py @@ -1,5 +1,5 @@ """ -An abstract class for component drivers. +Abstract classes for component drivers. """ import json @@ -24,6 +24,8 @@ from uwtools.scheduler import JobScheduler from uwtools.utils.processing import execute +# NB: Class docstrings are programmatically defined. + class Assets(ABC): """ @@ -38,15 +40,6 @@ def __init__( dry_run: bool = False, key_path: Optional[list[str]] = None, ) -> None: - """ - A component driver. - - :param cycle: The cycle. - :param leadtime: The leadtime. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param key_path: Keys leading through the config to the driver's configuration block. - """ self._config = YAMLConfig(config=config) self._config.dereference( context={ @@ -226,19 +219,11 @@ def __init__( dry_run: bool = False, key_path: Optional[list[str]] = None, ): - """ - The driver. - - :param cycle: The cycle. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param key_path: Keys leading through the config to the driver's configuration block. - """ super().__init__(cycle=cycle, config=config, dry_run=dry_run, key_path=key_path) self._cycle = cycle -class AssetsCycleAndLeadtimeBased(Assets): +class AssetsCycleLeadtimeBased(Assets): """ An abstract class to provision assets for cycle-and-leadtime-based components. """ @@ -251,15 +236,6 @@ def __init__( dry_run: bool = False, key_path: Optional[list[str]] = None, ): - """ - The driver. - - :param cycle: The cycle. - :param leadtime: The leadtime. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param key_path: Keys leading through the config to the driver's configuration block. - """ super().__init__( cycle=cycle, leadtime=leadtime, config=config, dry_run=dry_run, key_path=key_path ) @@ -278,13 +254,6 @@ def __init__( dry_run: bool = False, key_path: Optional[list[str]] = None, ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param key_path: Keys leading through the config to the driver's configuration block. - """ super().__init__(config=config, dry_run=dry_run, key_path=key_path) @@ -302,16 +271,6 @@ def __init__( key_path: Optional[list[str]] = None, batch: bool = False, ): - """ - The driver. - - :param cycle: The cycle. - :param leadtime: The leadtime. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param key_path: Keys leading through the config to the driver's configuration block. - :param batch: Run component via the batch system? - """ super().__init__( cycle=cycle, leadtime=leadtime, config=config, dry_run=dry_run, key_path=key_path ) @@ -490,22 +449,13 @@ def __init__( key_path: Optional[list[str]] = None, batch: bool = False, ): - """ - The driver. - - :param cycle: The cycle. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param key_path: Keys leading through the config to the driver's configuration block. - :param batch: Run component via the batch system? - """ super().__init__( cycle=cycle, config=config, dry_run=dry_run, key_path=key_path, batch=batch ) self._cycle = cycle -class DriverCycleAndLeadtimeBased(Driver): +class DriverCycleLeadtimeBased(Driver): """ An abstract class for standalone cycle-and-leadtime-based component drivers. """ @@ -519,16 +469,6 @@ def __init__( key_path: Optional[list[str]] = None, batch: bool = False, ): - """ - The driver. - - :param cycle: The cycle. - :param leadtime: The leadtime. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param key_path: Keys leading through the config to the driver's configuration block. - :param batch: Run component via the batch system? - """ super().__init__( cycle=cycle, leadtime=leadtime, @@ -553,15 +493,45 @@ def __init__( key_path: Optional[list[str]] = None, batch: bool = False, ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param key_path: Keys leading through the config to the driver's configuration block. - :param batch: Run component via the batch system? - """ super().__init__(config=config, dry_run=dry_run, key_path=key_path, batch=batch) DriverT = Union[type[Assets], type[Driver]] + + +def _add_docstring(class_: type, omit: Optional[list[str]] = None) -> None: + """ + Dynamically add docstring to a driver class. + + :param class_: The class to add the docstring to. + :param omit: Parameters to omit from the docstring. + """ + base = """ + The driver. + + :param cycle: The cycle. + :param leadtime: The leadtime. + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param key_path: Keys leading through the config to the driver's configuration block. + :param batch: Run component via the batch system? + """ + setattr( + class_, + "__doc__", + "\n".join( + line + for line in dedent(base).strip().split("\n") + if not any(line.startswith(f":param {o}:") for o in omit or []) + ), + ) + + +_add_docstring(Assets, omit=["batch"]) +_add_docstring(AssetsCycleBased, omit=["batch", "leadtime"]) +_add_docstring(AssetsCycleLeadtimeBased, omit=["batch"]) +_add_docstring(AssetsTimeInvariant, omit=["batch", "cycle", "leadtime"]) +_add_docstring(Driver) +_add_docstring(DriverCycleBased, omit=["leadtime"]) +_add_docstring(DriverCycleLeadtimeBased) +_add_docstring(DriverTimeInvariant, omit=["cycle", "leadtime"]) diff --git a/src/uwtools/drivers/upp.py b/src/uwtools/drivers/upp.py index 866d34ca4..2b56eb649 100644 --- a/src/uwtools/drivers/upp.py +++ b/src/uwtools/drivers/upp.py @@ -7,12 +7,12 @@ from iotaa import asset, task, tasks from uwtools.config.formats.nml import NMLConfig -from uwtools.drivers.driver import DriverCycleAndLeadtimeBased +from uwtools.drivers.driver import DriverCycleLeadtimeBased from uwtools.strings import STR from uwtools.utils.tasks import file, filecopy, symlink -class UPP(DriverCycleAndLeadtimeBased): +class UPP(DriverCycleLeadtimeBased): """ A driver for UPP. """ diff --git a/src/uwtools/tests/config/formats/test_yaml.py b/src/uwtools/tests/config/formats/test_yaml.py index 93673128b..415785c2f 100644 --- a/src/uwtools/tests/config/formats/test_yaml.py +++ b/src/uwtools/tests/config/formats/test_yaml.py @@ -133,7 +133,7 @@ def test_yaml_constructor_error_not_dict_from_file(tmp_path): def test_yaml_constructor_error_not_dict_from_stdin(): # Test that a useful exception is raised if the YAML stdin input is a non-dict value. - with patch.object(sys, "stdin", new=StringIO("a string")): + with StringIO("a string") as sio, patch.object(sys, "stdin", new=sio): with raises(exceptions.UWConfigError) as e: YAMLConfig() assert "Parsed a str value from stdin, expected a dict" in str(e.value) @@ -167,9 +167,10 @@ def test_yaml_stdin_plus_relpath_failure(caplog): log.setLevel(logging.INFO) _stdinproxy.cache_clear() relpath = "../bar/baz.yaml" - with patch.object(sys, "stdin", new=StringIO(f"foo: {support.INCLUDE_TAG} [{relpath}]")): - with raises(UWConfigError) as e: - YAMLConfig() + with StringIO(f"foo: {support.INCLUDE_TAG} [{relpath}]") as sio: + with patch.object(sys, "stdin", new=sio): + with raises(UWConfigError) as e: + YAMLConfig() msg = f"Reading from stdin, a relative path was encountered: {relpath}" assert msg in str(e.value) assert logged(caplog, msg) diff --git a/src/uwtools/tests/config/test_atparse_to_jinja2.py b/src/uwtools/tests/config/test_atparse_to_jinja2.py index 4c6df7745..06bf9f86e 100644 --- a/src/uwtools/tests/config/test_atparse_to_jinja2.py +++ b/src/uwtools/tests/config/test_atparse_to_jinja2.py @@ -104,7 +104,7 @@ def test_convert_preserve_whitespace(tmp_path): def test_convert_stdin_to_file(txt_atparse, capsys, txt_jinja2, tmp_path): outfile = tmp_path / "outfile" _stdinproxy.cache_clear() - with patch.object(sys, "stdin", new=StringIO(txt_atparse)): + with StringIO(txt_atparse) as sio, patch.object(sys, "stdin", new=sio): atparse_to_jinja2.convert(output_file=outfile) with open(outfile, "r", encoding="utf-8") as f: assert f.read().strip() == txt_jinja2 @@ -117,7 +117,7 @@ def test_convert_stdin_to_logging(txt_atparse, caplog, txt_jinja2, tmp_path): log.setLevel(logging.INFO) outfile = tmp_path / "outfile" _stdinproxy.cache_clear() - with patch.object(sys, "stdin", new=StringIO(txt_atparse)): + with StringIO(txt_atparse) as sio, patch.object(sys, "stdin", new=sio): atparse_to_jinja2.convert(output_file=outfile, dry_run=True) assert "\n".join(record.message for record in caplog.records) == txt_jinja2 assert not outfile.is_file() @@ -125,7 +125,7 @@ def test_convert_stdin_to_logging(txt_atparse, caplog, txt_jinja2, tmp_path): def test_convert_stdin_to_stdout(txt_atparse, capsys, txt_jinja2): _stdinproxy.cache_clear() - with patch.object(sys, "stdin", new=StringIO(txt_atparse)): + with StringIO(txt_atparse) as sio, patch.object(sys, "stdin", new=sio): atparse_to_jinja2.convert() streams = capsys.readouterr() assert not streams.err diff --git a/src/uwtools/tests/config/test_jinja2.py b/src/uwtools/tests/config/test_jinja2.py index 186472464..09d6df110 100644 --- a/src/uwtools/tests/config/test_jinja2.py +++ b/src/uwtools/tests/config/test_jinja2.py @@ -528,16 +528,16 @@ def test_searchpath_file_two_paths(self, searchpath_assets): def test_searchpath_stdin_default(self, searchpath_assets): # There is no default search path for reads from stdin: a = searchpath_assets - with patch.object(jinja2, "readable") as readable: - readable.return_value.__enter__.return_value = StringIO(a.s1) + with patch.object(jinja2, "readable") as readable, StringIO(a.s1) as sio: + readable.return_value.__enter__.return_value = sio with raises(TemplateNotFound): J2Template(values={}).render() def test_searchpath_stdin_explicit(self, searchpath_assets): # An explicit search path is honored when reading from stdin: a = searchpath_assets - with patch.object(jinja2, "readable") as readable: - readable.return_value.__enter__.return_value = StringIO(a.s1) + with patch.object(jinja2, "readable") as readable, StringIO(a.s1) as sio: + readable.return_value.__enter__.return_value = sio assert J2Template(values={}, searchpath=[a.d1]).render() == "2" def test_undeclared_variables(self): diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 94a3f0873..f78ca3d12 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -640,11 +640,11 @@ def test__realize_config_input_setup_ini_stdin(caplog): """ stdinproxy.cache_clear() log.setLevel(logging.DEBUG) - s = StringIO() - print(dedent(data).strip(), file=s) - s.seek(0) - with patch.object(sys, "stdin", new=s): - input_obj = tools._realize_config_input_setup(input_format=FORMAT.ini) + with StringIO() as sio: + print(dedent(data).strip(), file=sio) + sio.seek(0) + with patch.object(sys, "stdin", new=sio): + input_obj = tools._realize_config_input_setup(input_format=FORMAT.ini) assert input_obj.data == {"section": {"foo": "bar", "baz": "88"}} # note: 88 is str, not int assert logged(caplog, "Reading input from stdin") @@ -677,11 +677,11 @@ def test__realize_config_input_setup_nml_stdin(caplog): """ stdinproxy.cache_clear() log.setLevel(logging.DEBUG) - s = StringIO() - print(dedent(data).strip(), file=s) - s.seek(0) - with patch.object(sys, "stdin", new=s): - input_obj = tools._realize_config_input_setup(input_format=FORMAT.nml) + with StringIO() as sio: + print(dedent(data).strip(), file=sio) + sio.seek(0) + with patch.object(sys, "stdin", new=sio): + input_obj = tools._realize_config_input_setup(input_format=FORMAT.nml) assert input_obj["nl"]["pi"] == 3.14 assert logged(caplog, "Reading input from stdin") @@ -710,11 +710,11 @@ def test__realize_config_input_setup_sh_stdin(caplog): """ stdinproxy.cache_clear() log.setLevel(logging.DEBUG) - s = StringIO() - print(dedent(data).strip(), file=s) - s.seek(0) - with patch.object(sys, "stdin", new=s): - input_obj = tools._realize_config_input_setup(input_format=FORMAT.sh) + with StringIO() as sio: + print(dedent(data).strip(), file=sio) + sio.seek(0) + with patch.object(sys, "stdin", new=sio): + input_obj = tools._realize_config_input_setup(input_format=FORMAT.sh) assert input_obj.data == {"foo": "bar"} assert logged(caplog, "Reading input from stdin") @@ -743,11 +743,11 @@ def test__realize_config_input_setup_yaml_stdin(caplog): """ stdinproxy.cache_clear() log.setLevel(logging.DEBUG) - s = StringIO() - print(dedent(data).strip(), file=s) - s.seek(0) - with patch.object(sys, "stdin", new=s): - input_obj = tools._realize_config_input_setup(input_format=FORMAT.yaml) + with StringIO() as sio: + print(dedent(data).strip(), file=sio) + sio.seek(0) + with patch.object(sys, "stdin", new=sio): + input_obj = tools._realize_config_input_setup(input_format=FORMAT.yaml) assert input_obj.data == {"foo": "bar"} assert logged(caplog, "Reading input from stdin") @@ -773,13 +773,13 @@ def test__realize_config_update_stdin(caplog, realize_config_testobj): stdinproxy.cache_clear() log.setLevel(logging.DEBUG) assert realize_config_testobj[1][2][3] == 88 - s = StringIO() - print("{1: {2: {3: 99}}}", file=s) - s.seek(0) - with patch.object(sys, "stdin", new=s): - o = tools._realize_config_update( - input_obj=realize_config_testobj, update_format=FORMAT.yaml - ) + with StringIO() as sio: + print("{1: {2: {3: 99}}}", file=sio) + sio.seek(0) + with patch.object(sys, "stdin", new=sio): + o = tools._realize_config_update( + input_obj=realize_config_testobj, update_format=FORMAT.yaml + ) assert o[1][2][3] == 99 assert logged(caplog, "Reading update from stdin") diff --git a/src/uwtools/tests/drivers/test_driver.py b/src/uwtools/tests/drivers/test_driver.py index fdd0c2b51..8285b2f26 100644 --- a/src/uwtools/tests/drivers/test_driver.py +++ b/src/uwtools/tests/drivers/test_driver.py @@ -50,7 +50,7 @@ class ConcreteAssetsCycleBased(Common, driver.AssetsCycleBased): pass -class ConcreteAssetsCycleAndLeadtimeBased(Common, driver.AssetsCycleAndLeadtimeBased): +class ConcreteAssetsCycleLeadtimeBased(Common, driver.AssetsCycleLeadtimeBased): pass @@ -62,7 +62,7 @@ class ConcreteDriverCycleBased(Common, driver.DriverCycleBased): pass -class ConcreteDriverCycleAndLeadtimeBased(Common, driver.DriverCycleAndLeadtimeBased): +class ConcreteDriverCycleLeadtimeBased(Common, driver.DriverCycleLeadtimeBased): pass @@ -130,7 +130,7 @@ def test_Assets_repr_cycle_based(config): def test_Assets_repr_cycle_and_leadtime_based(config): - obj = ConcreteAssetsCycleAndLeadtimeBased( + obj = ConcreteAssetsCycleLeadtimeBased( config=config, cycle=dt.datetime(2024, 7, 2, 12), leadtime=dt.timedelta(hours=6) ) expected = "concrete 2024-07-02T12:00 06:00:00 in %s" % obj._driver_config["rundir"] @@ -517,3 +517,15 @@ def test_Driver__write_runscript(driverobj): with open(path, "r", encoding="utf-8") as f: actual = f.read() assert actual.strip() == dedent(expected).strip() + + +def test__add_docstring(): + class C: + pass + + assert getattr(C, "__doc__") is None + with patch.object(driver, "C", C, create=True): + class_ = driver.C # type: ignore # pylint: disable=no-member + omit = ["cycle", "leadtime", "config", "dry_run", "key_path", "batch"] + driver._add_docstring(class_=class_, omit=omit) + assert getattr(C, "__doc__").strip() == "The driver." diff --git a/src/uwtools/tests/utils/test_api.py b/src/uwtools/tests/utils/test_api.py index 89ba2ac67..13335e07f 100644 --- a/src/uwtools/tests/utils/test_api.py +++ b/src/uwtools/tests/utils/test_api.py @@ -8,7 +8,7 @@ from pytest import fixture, mark, raises from uwtools.exceptions import UWError -from uwtools.tests.drivers.test_driver import ConcreteDriverCycleAndLeadtimeBased as TestDriverWCL +from uwtools.tests.drivers.test_driver import ConcreteDriverCycleLeadtimeBased as TestDriverWCL from uwtools.tests.drivers.test_driver import ConcreteDriverTimeInvariant as TestDriver from uwtools.utils import api diff --git a/src/uwtools/tests/utils/test_file.py b/src/uwtools/tests/utils/test_file.py index 1151e5493..d33282a61 100644 --- a/src/uwtools/tests/utils/test_file.py +++ b/src/uwtools/tests/utils/test_file.py @@ -28,11 +28,11 @@ def assets(tmp_path): def test_StdinProxy(): msg = "proxying stdin" - with patch.object(sys, "stdin", new=StringIO(msg)): + with StringIO(msg) as sio, patch.object(sys, "stdin", new=sio): assert sys.stdin.read() == msg # Reading from stdin a second time yields no input, as the stream has been exhausted: assert sys.stdin.read() == "" - with patch.object(sys, "stdin", new=StringIO(msg)): + with StringIO(msg) as sio, patch.object(sys, "stdin", new=sio): sp = file.StdinProxy() assert sp.read() == msg # But the stdin proxy can be read multiple times: @@ -47,14 +47,14 @@ def test__stdinproxy(): msg0 = "hello world" msg1 = "bonjour monde" # Unsurprisingly, the first read from stdin finds the expected message: - with patch.object(sys, "stdin", new=StringIO(msg0)): + with StringIO(msg0) as sio, patch.object(sys, "stdin", new=sio): assert file._stdinproxy().read() == msg0 # But after re-patching stdin with a new message, a second read returns the old message: - with patch.object(sys, "stdin", new=StringIO(msg1)): + with StringIO(msg1) as sio, patch.object(sys, "stdin", new=sio): assert file._stdinproxy().read() == msg0 # <-- the OLD message # However, if the cache is cleared, the second message is then read: file._stdinproxy.cache_clear() - with patch.object(sys, "stdin", new=StringIO(msg1)): + with StringIO(msg1) as sio, patch.object(sys, "stdin", new=sio): assert file._stdinproxy().read() == msg1 # <-- the NEW message @@ -104,9 +104,8 @@ def test_readable_file(tmp_path): def test_readable_nofile(): file._stdinproxy.cache_clear() - with patch.object(sys, "stdin", new=StringIO("hello")): - with file.readable() as f: - assert f.read() == "hello" + with StringIO("hello") as sio, patch.object(sys, "stdin", new=sio), file.readable() as f: + assert f.read() == "hello" def test_resource_path():