From db10a4d872450eac0441a8f873c2e0c0eba78889 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 29 Apr 2024 04:20:37 -0400 Subject: [PATCH] Prefer pathlib for read/write operations (#591) * Rely on pathlib when reading and writing simple text. * Universal newlines is the default, so remove the comment. * Add news fragment. * Add compatibility shim for Python 3.9 and earlier. * Suppress coverage check for line 20 as it emits a spurious failure. * Only exclude branch coverage (regular coverage is fine) Co-authored-by: Adi Roiban * Add comment explaining motivation for the compatibility shim. --------- Co-authored-by: Adi Roiban --- src/towncrier/_builder.py | 4 +-- src/towncrier/_writer.py | 36 ++++++++++++++++-------- src/towncrier/build.py | 5 ++-- src/towncrier/create.py | 10 ++++--- src/towncrier/newsfragments/591.misc.rst | 1 + 5 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 src/towncrier/newsfragments/591.misc.rst diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index fefdf838..3a4591de 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -8,6 +8,7 @@ import textwrap from collections import defaultdict +from pathlib import Path from typing import Any, DefaultDict, Iterable, Iterator, Mapping, Sequence from jinja2 import Template @@ -111,8 +112,7 @@ def find_fragments( full_filename = os.path.join(section_dir, basename) fragment_filenames.append(full_filename) - with open(full_filename, "rb") as f: - data = f.read().decode("utf-8", "replace") + data = Path(full_filename).read_text(encoding="utf-8", errors="replace") if (ticket, category, counter) in file_content: raise ValueError( diff --git a/src/towncrier/_writer.py b/src/towncrier/_writer.py index 6bbc5bfa..634b5f19 100644 --- a/src/towncrier/_writer.py +++ b/src/towncrier/_writer.py @@ -8,7 +8,22 @@ from __future__ import annotations +import sys + from pathlib import Path +from typing import Any + + +if sys.version_info < (3, 10): + # Compatibility shim for newline parameter to write_text, added in 3.10 + def _newline_write_text(path: Path, content: str, **kwargs: Any) -> None: + with path.open("w", **kwargs) as strm: # pragma: no branch + strm.write(content) + +else: + + def _newline_write_text(path: Path, content: str, **kwargs: Any) -> None: + path.write_text(content, **kwargs) def append_to_newsfile( @@ -37,15 +52,17 @@ def append_to_newsfile( if top_line and top_line in prev_body: raise ValueError("It seems you've already produced newsfiles for this version?") - # Leave newlines alone. This probably leads to inconsistent newlines, - # because we've loaded existing content with universal newlines, but that's - # the original behavior. - with news_file.open("w", encoding="utf-8", newline="") as f: - if header: - f.write(header) + _newline_write_text( + news_file, # If there is no previous body that means we're writing a brand new news file. # We don't want extra whitespace at the end of this new file. - f.write(content + prev_body if prev_body else content.rstrip() + "\n") + header + (content + prev_body if prev_body else content.rstrip() + "\n"), + encoding="utf-8", + # Leave newlines alone. This probably leads to inconsistent newlines, + # because we've loaded existing content with universal newlines, but that's + # the original behavior. + newline="", + ) def _figure_out_existing_content( @@ -64,10 +81,7 @@ def _figure_out_existing_content( # Non-existent files have no existing content. return "", "" - # If we didn't use universal newlines here, we wouldn't find *start_string* - # which usually contains a `\n`. - with news_file.open(encoding="utf-8") as f: - content = f.read() + content = Path(news_file).read_text(encoding="utf-8") t = content.split(start_string, 1) if len(t) == 2: diff --git a/src/towncrier/build.py b/src/towncrier/build.py index f8e4175e..b28606c9 100644 --- a/src/towncrier/build.py +++ b/src/towncrier/build.py @@ -5,13 +5,13 @@ Build a combined news file from news fragments. """ - from __future__ import annotations import os import sys from datetime import date +from pathlib import Path import click @@ -171,8 +171,7 @@ def __main( .read_text(encoding="utf-8") ) else: - with open(config.template, encoding="utf-8") as tmpl: - template = tmpl.read() + template = Path(config.template).read_text(encoding="utf-8") click.echo("Finding news fragments...", err=to_err) diff --git a/src/towncrier/create.py b/src/towncrier/create.py index 2d2ec65b..28903f58 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -9,6 +9,8 @@ import os +from pathlib import Path + import click from ._settings import config_option_help, load_config_from_options @@ -170,10 +172,10 @@ def __main( click.echo("Aborted creating news fragment due to empty message.") ctx.exit(1) - with open(segment_file, "w", encoding="utf-8") as f: - f.write(content) - if config.create_eof_newline and content and not content.endswith("\n"): - f.write("\n") + add_newline = bool( + config.create_eof_newline and content and not content.endswith("\n") + ) + Path(segment_file).write_text(content + "\n" * add_newline, encoding="utf-8") click.echo(f"Created news fragment at {segment_file}") diff --git a/src/towncrier/newsfragments/591.misc.rst b/src/towncrier/newsfragments/591.misc.rst new file mode 100644 index 00000000..10f5e3a7 --- /dev/null +++ b/src/towncrier/newsfragments/591.misc.rst @@ -0,0 +1 @@ +Leveraged pathlib in most file operations.