Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encoding warning use PEP 597 env var PYTHONWARNDEFAULTENCODING #733

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
Expand Down
6 changes: 1 addition & 5 deletions src/monty/bisect.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
from __future__ import annotations

import bisect as bs
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Optional

__author__ = "Matteo Giantomassi"
__copyright__ = "Copyright 2013, The Materials Virtual Lab"
Expand All @@ -23,7 +19,7 @@
__date__ = "11/09/14"


def index(a: list[float], x: float, atol: Optional[float] = None) -> int:
def index(a: list[float], x: float, atol: float | None = None) -> int:
"""Locate the leftmost value exactly equal to x."""
i = bs.bisect_left(a, x)
if i != len(a):
Expand Down
8 changes: 4 additions & 4 deletions src/monty/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Callable, Optional, Type
from typing import Callable, Type

logger = logging.getLogger(__name__)


def deprecated(
replacement: Optional[Callable | str] = None,
replacement: Callable | str | None = None,
message: str = "",
deadline: Optional[tuple[int, int, int]] = None,
deadline: tuple[int, int, int] | None = None,
category: Type[Warning] = FutureWarning,
) -> Callable:
"""
Expand All @@ -34,7 +34,7 @@ def deprecated(
Args:
replacement (Callable | str): A replacement class or function.
message (str): A warning message to be displayed.
deadline (Optional[tuple[int, int, int]]): Optional deadline for removal
deadline (tuple[int, int, int] | None): Optional deadline for removal
of the old function/class, in format (yyyy, MM, dd). A CI warning would
be raised after this date if is running in code owner' repo.
category (Warning): Choose the category of the warning to issue. Defaults
Expand Down
4 changes: 2 additions & 2 deletions src/monty/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, Callable, Union
from typing import Any, Callable


class _HashedSeq(list): # pylint: disable=C0205
Expand Down Expand Up @@ -130,7 +130,7 @@ def invalidate(cls, inst: object, name: str) -> None:


def return_if_raise(
exception_tuple: Union[list, tuple], retval_if_exc: Any, disabled: bool = False
exception_tuple: list | tuple, retval_if_exc: Any, disabled: bool = False
) -> Any:
"""
Decorator for functions, methods or properties. Execute the callable in a
Expand Down
36 changes: 18 additions & 18 deletions src/monty/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@
from typing import TYPE_CHECKING, Literal, cast

if TYPE_CHECKING:
from typing import IO, Any, Iterator, Union


class EncodingWarning(Warning): ... # Added in Python 3.10
from typing import IO, Any, Iterator


def zopen(
filename: Union[str, Path],
filename: str | Path,
/,
mode: str | None = None,
**kwargs: Any,
Expand All @@ -35,19 +32,20 @@ def zopen(
This function wraps around `[bz2/gzip/lzma].open` and `open`
to deal intelligently with compressed or uncompressed files.
Supports context manager:
`with zopen(filename, mode="rt", ...)`.
`with zopen(filename, mode="rt", ...)`

Important Notes:
- Default `mode` should not be used, and would not be allow
in future versions.
- Always explicitly specify binary/text in `mode`, i.e.
always pass `t` or `b` in `mode`, implicit binary/text
mode would not be allow in future versions.
- Always provide an explicit `encoding` in text mode.
- Always provide an explicit `encoding` in text mode, it would
be set to UTF-8 by default otherwise.

Args:
filename (str | Path): The file to open.
mode (str): The mode in which the file is opened, you MUST
mode (str): The mode in which the file is opened, you should
explicitly specify "b" for binary or "t" for text.
**kwargs: Additional keyword arguments to pass to `open`.

Expand Down Expand Up @@ -79,14 +77,16 @@ def zopen(
stacklevel=2,
)

# Warn against default `encoding` in text mode
# Warn against default `encoding` in text mode if
# `PYTHONWARNDEFAULTENCODING` environment variable is set (PEP 597)
if "t" in mode and kwargs.get("encoding", None) is None:
warnings.warn(
"We strongly encourage explicit `encoding`, "
"and we would use UTF-8 by default as per PEP 686",
category=EncodingWarning,
stacklevel=2,
)
if os.getenv("PYTHONWARNDEFAULTENCODING", False):
warnings.warn(
"We strongly encourage explicit `encoding`, "
"and we would use UTF-8 by default as per PEP 686",
category=EncodingWarning,
stacklevel=2,
)
kwargs["encoding"] = "utf-8"

_name, ext = os.path.splitext(filename)
Expand Down Expand Up @@ -141,7 +141,7 @@ def _get_line_ending(
If file is empty, "\n" would be used as default.
"""
if isinstance(file, (str, Path)):
with zopen(file, "rb") as f:
with zopen(file, mode="rb") as f:
first_line = f.readline()
elif isinstance(file, io.TextIOWrapper):
first_line = file.buffer.readline() # type: ignore[attr-defined]
Expand Down Expand Up @@ -169,7 +169,7 @@ def _get_line_ending(


def reverse_readfile(
filename: Union[str, Path],
filename: str | Path,
) -> Iterator[str]:
"""
A much faster reverse read of file by using Python's mmap to generate a
Expand All @@ -187,7 +187,7 @@ def reverse_readfile(
l_end = _get_line_ending(filename)
len_l_end = len(l_end)

with zopen(filename, "rb") as file:
with zopen(filename, mode="rb") as file:
if isinstance(file, (gzip.GzipFile, bz2.BZ2File)):
for line in reversed(file.readlines()):
# "readlines" would keep the line end character
Expand Down
6 changes: 3 additions & 3 deletions src/monty/os/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

if TYPE_CHECKING:
from pathlib import Path
from typing import Generator, Union
from typing import Generator

__author__ = "Shyue Ping Ong"
__copyright__ = "Copyright 2013, The Materials Project"
Expand All @@ -22,7 +22,7 @@


@contextmanager
def cd(path: Union[str, Path]) -> Generator:
def cd(path: str | Path) -> Generator:
"""
A Fabric-inspired cd context that temporarily changes directory for
performing some tasks, and returns to the original working directory
Expand All @@ -42,7 +42,7 @@ def cd(path: Union[str, Path]) -> Generator:
os.chdir(cwd)


def makedirs_p(path: Union[str, Path], **kwargs) -> None:
def makedirs_p(path: str | Path, **kwargs) -> None:
"""
Wrapper for os.makedirs that does not raise an exception if the directory
already exists, in the fashion of "mkdir -p" command. The check is
Expand Down
8 changes: 4 additions & 4 deletions src/monty/os/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from monty.string import list_strings

if TYPE_CHECKING:
from typing import Callable, Literal, Optional, Union
from typing import Callable, Literal


def zpath(filename: str | Path) -> str:
Expand Down Expand Up @@ -41,9 +41,9 @@ def zpath(filename: str | Path) -> str:

def find_exts(
top: str,
exts: Union[str, list[str]],
exclude_dirs: Optional[str] = None,
include_dirs: Optional[str] = None,
exts: str | list[str],
exclude_dirs: str | None = None,
include_dirs: str | None = None,
match_mode: Literal["basename", "abspath"] = "basename",
) -> list[str]:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/monty/re.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def regrep(
gen = (
reverse_readfile(filename)
if reverse
else zopen(filename, "rt", encoding="utf-8")
else zopen(filename, mode="rt", encoding="utf-8")
)
for i, line in enumerate(gen):
for k, p in compiled.items():
Expand Down
32 changes: 22 additions & 10 deletions src/monty/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@

if TYPE_CHECKING:
from pathlib import Path
from typing import Any, Optional, TextIO, Union
from typing import Any, Literal, TextIO


def loadfn(fn: Union[str, Path], *args, fmt: Optional[str] = None, **kwargs) -> Any:
def loadfn(
fn: str | Path,
*args,
fmt: Literal["json", "yaml", "mpk"] | None = None,
**kwargs,
) -> Any:
"""
Loads json/yaml/msgpack directly from a filename instead of a
File-like object. File may also be a BZ2 (".BZ2") or GZIP (".GZ", ".Z")
Expand All @@ -39,9 +44,8 @@ def loadfn(fn: Union[str, Path], *args, fmt: Optional[str] = None, **kwargs) ->
Args:
fn (str/Path): filename or pathlib.Path.
*args: Any of the args supported by json/yaml.load.
fmt (string): If specified, the fmt specified would be used instead
of autodetection from filename. Supported formats right now are
"json", "yaml" or "mpk".
fmt ("json" | "yaml" | "mpk"): If specified, the fmt specified would
be used instead of autodetection from filename.
**kwargs: Any of the kwargs supported by json/yaml.load.

Returns:
Expand All @@ -64,10 +68,10 @@ def loadfn(fn: Union[str, Path], *args, fmt: Optional[str] = None, **kwargs) ->
)
if "object_hook" not in kwargs:
kwargs["object_hook"] = object_hook
with zopen(fn, "rb") as fp:
with zopen(fn, mode="rb") as fp:
return msgpack.load(fp, *args, **kwargs) # pylint: disable=E1101
else:
with zopen(fn, "rt", encoding="utf-8") as fp:
with zopen(fn, mode="rt", encoding="utf-8") as fp:
if fmt == "yaml":
if YAML is None:
raise RuntimeError("Loading of YAML files requires ruamel.yaml.")
Expand All @@ -81,7 +85,13 @@ def loadfn(fn: Union[str, Path], *args, fmt: Optional[str] = None, **kwargs) ->
raise TypeError(f"Invalid format: {fmt}")


def dumpfn(obj: object, fn: Union[str, Path], *args, fmt=None, **kwargs) -> None:
def dumpfn(
obj: object,
fn: str | Path,
*args,
fmt: Literal["json", "yaml", "mpk"] | None = None,
**kwargs,
) -> None:
"""
Dump to a json/yaml directly by filename instead of a
File-like object. File may also be a BZ2 (".BZ2") or GZIP (".GZ", ".Z")
Expand All @@ -95,6 +105,8 @@ def dumpfn(obj: object, fn: Union[str, Path], *args, fmt=None, **kwargs) -> None
Args:
obj (object): Object to dump.
fn (str/Path): filename or pathlib.Path.
fmt ("json" | "yaml" | "mpk"): If specified, the fmt specified would
be used instead of autodetection from filename.
*args: Any of the args supported by json/yaml.dump.
**kwargs: Any of the kwargs supported by json/yaml.dump.

Expand All @@ -117,10 +129,10 @@ def dumpfn(obj: object, fn: Union[str, Path], *args, fmt=None, **kwargs) -> None
)
if "default" not in kwargs:
kwargs["default"] = default
with zopen(fn, "wb") as fp:
with zopen(fn, mode="wb") as fp:
msgpack.dump(obj, fp, *args, **kwargs) # pylint: disable=E1101
else:
with zopen(fn, "wt", encoding="utf-8") as fp:
with zopen(fn, mode="wt", encoding="utf-8") as fp:
fp = cast(TextIO, fp)

if fmt == "yaml":
Expand Down
10 changes: 5 additions & 5 deletions src/monty/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from monty.io import zopen

if TYPE_CHECKING:
from typing import Literal, Optional
from typing import Literal


def copy_r(src: str | Path, dst: str | Path) -> None:
Expand Down Expand Up @@ -76,7 +76,7 @@ def gzip_dir(path: str | Path, compresslevel: int = 6) -> None:
def compress_file(
filepath: str | Path,
compression: Literal["gz", "bz2"] = "gz",
target_dir: Optional[str | Path] = None,
target_dir: str | Path | None = None,
) -> None:
"""
Compresses a file with the correct extension. Functions like standard
Expand Down Expand Up @@ -104,7 +104,7 @@ def compress_file(
else:
compressed_file = f"{str(filepath)}.{compression}"

with open(filepath, "rb") as f_in, zopen(compressed_file, "wb") as f_out:
with open(filepath, "rb") as f_in, zopen(compressed_file, mode="wb") as f_out:
f_out.writelines(f_in)

os.remove(filepath)
Expand All @@ -130,7 +130,7 @@ def compress_dir(path: str | Path, compression: Literal["gz", "bz2"] = "gz") ->


def decompress_file(
filepath: str | Path, target_dir: Optional[str | Path] = None
filepath: str | Path, target_dir: str | Path | None = None
) -> str | None:
"""
Decompresses a file with the correct extension. Automatically detects
Expand All @@ -157,7 +157,7 @@ def decompress_file(
else:
decompressed_file = str(filepath).removesuffix(file_ext)

with zopen(filepath, "rb") as f_in, open(decompressed_file, "wb") as f_out:
with zopen(filepath, mode="rb") as f_in, open(decompressed_file, "wb") as f_out:
f_out.writelines(f_in)

os.remove(filepath)
Expand Down
4 changes: 2 additions & 2 deletions src/monty/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import TYPE_CHECKING, Iterable, cast

if TYPE_CHECKING:
from typing import Any, Union
from typing import Any


def remove_non_ascii(s: str) -> str:
Expand All @@ -34,7 +34,7 @@ def is_string(s: Any) -> bool:
return False


def list_strings(arg: Union[str, Iterable[str]]) -> list[str]:
def list_strings(arg: str | Iterable[str]) -> list[str]:
"""
Always return a list of strings, given a string or list of strings as
input.
Expand Down
4 changes: 1 addition & 3 deletions src/monty/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
from monty.string import is_string

if TYPE_CHECKING:
from typing import Optional

from typing_extensions import Self

__author__ = "Matteo Giantomass"
Expand Down Expand Up @@ -63,7 +61,7 @@ def __init__(self, command: str):
def __str__(self):
return f"command: {self.command}, retcode: {self.retcode}"

def run(self, timeout: Optional[float] = None, **kwargs) -> Self:
def run(self, timeout: float | None = None, **kwargs) -> Self:
"""
Run a command in a separated thread and wait timeout seconds.
kwargs are keyword arguments passed to Popen.
Expand Down
Loading
Loading