diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8b5f45b0..77a3f9a71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,17 +9,17 @@ jobs: max-parallel: 20 matrix: os: [ubuntu-latest, macos-14, windows-latest] - python-version: ["3.10", "3.12"] + python-version: ["3.9", "3.12"] runs-on: ${{ matrix.os }} 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: | @@ -30,6 +30,6 @@ jobs: run: pytest --cov=monty --cov-report html:coverage_reports tests - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1b5a3167..8fd3c15fd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.6.9 hooks: - id: ruff args: [--fix] @@ -24,7 +24,7 @@ repos: exclude: ^tests - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.11.2 hooks: - id: mypy @@ -37,19 +37,19 @@ repos: additional_dependencies: [tomli] # needed to read pyproject.toml below py3.11 - repo: https://github.com/MarcoGorelli/cython-lint - rev: v0.16.6 + rev: v0.16.2 hooks: - id: cython-lint args: [--no-pycodestyle] - id: double-quote-cython-strings - repo: https://github.com/adamchainz/blacken-docs - rev: 1.19.1 + rev: 1.19.0 hooks: - id: blacken-docs - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.43.0 + rev: v0.42.0 hooks: - id: markdownlint # MD013: line too long diff --git a/README.md b/README.md index 3b5d01ff6..08be028f0 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,6 @@ Monty is created to serve as a complement to the Python standard library. It provides suite of tools to solve many common problems, and hopefully, be a resource to collect the best solutions. -Monty supports Python 3.10+. +Monty supports Python 3.x. Please visit the [official docs](https://materialsvirtuallab.github.io/monty) for more information. diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index adaf27057..94d13ee61 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -224,7 +224,8 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rexml (3.3.9) + rexml (3.3.6) + strscan rouge (3.26.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -239,6 +240,7 @@ GEM faraday (>= 0.17.3, < 3) simpleidn (0.2.1) unf (~> 0.1.4) + strscan (3.1.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) typhoeus (1.4.0) diff --git a/pylintrc b/pylintrc index d339072e0..932c509e4 100644 --- a/pylintrc +++ b/pylintrc @@ -80,7 +80,7 @@ persistent=yes # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. -py-version=3.10 +py-version=3.9 # Discover python modules and packages in the file system subtree. recursive=no diff --git a/pyproject.toml b/pyproject.toml index 0bbdfe3b5..7fddce0eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ maintainers = [ ] description = "Monty is the missing complement to Python." readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", @@ -20,7 +20,7 @@ classifiers = [ ] dependencies = [ "ruamel.yaml", - "numpy", + "numpy<2.0.0", ] version = "2024.10.21" @@ -31,7 +31,6 @@ ci = [ "pytest>=8", "pytest-cov>=4", "types-requests", - "pymongo" ] # dev is for "dev" module, not for development dev = ["ipython"] @@ -40,14 +39,12 @@ docs = [ "sphinx_rtd_theme", ] json = [ - "pymongo", + "bson", "orjson>=3.6.1", "pandas", "pydantic", - # https://github.com/hgrecco/pint/issues/2065 - "pint; python_version<'3.13'", - # Note: need torch>=2.3.0 for numpy 2 # 719 - "torch; python_version<'3.13'", # python 3.13 not supported yet + "pint", + "torch", ] multiprocessing = ["tqdm"] optional = ["monty[dev,json,multiprocessing,serialization]"] @@ -60,9 +57,10 @@ include = ["monty", "monty.*"] [tool.black] line-length = 120 -target-version = ["py310"] +target-version = ['py39'] include = '\.pyi?$' exclude = ''' + ( /( \.eggs # exclude a few common directories in the @@ -87,6 +85,8 @@ branch = true exclude_also = [ "@deprecated", "def __repr__", + "if 0:", + "if __name__ == .__main__.:", "if self.debug:", "if settings.DEBUG", "pragma: no cover", @@ -94,17 +94,17 @@ exclude_also = [ "raise NotImplementedError", "show_plot", "if TYPE_CHECKING:", + "if typing.TYPE_CHECKING:", "except ImportError:" ] [tool.mypy] ignore_missing_imports = true -[tool.ruff.lint] -select = [ - "I", # isort +[tool.ruff] +lint.select = [ + "I", #isort ] -[tool.ruff.lint.isort] -required-imports = ["from __future__ import annotations"] -known-first-party = ["monty"] +lint.isort.required-imports = ["from __future__ import annotations"] +lint.isort.known-first-party = ["monty"] diff --git a/src/monty/collections.py b/src/monty/collections.py index 154a9c863..812bff832 100644 --- a/src/monty/collections.py +++ b/src/monty/collections.py @@ -1,27 +1,17 @@ """ -Useful collection classes: - - tree: A recursive `defaultdict` for creating nested dictionaries - with default values. - - ControlledDict: A base dict class with configurable mutability. - - frozendict: An immutable dictionary. - - Namespace: A dict doesn't allow changing values, but could - add new keys, - - AttrDict: A dict whose values could be access as `dct.key`. - - FrozenAttrDict: An immutable version of `AttrDict`. - - MongoDict: A dict-like object whose values are nested dicts - could be accessed as attributes. +Useful collection classes, e.g., tree, frozendict, etc. """ from __future__ import annotations import collections -import warnings -from abc import ABC from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, Iterable + from typing_extensions import Self + def tree() -> collections.defaultdict: """ @@ -30,7 +20,7 @@ def tree() -> collections.defaultdict: Usage: x = tree() - x["a"]["b"]["c"] = 1 + x['a']['b']['c'] = 1 Returns: A tree. @@ -38,194 +28,118 @@ def tree() -> collections.defaultdict: return collections.defaultdict(tree) -class ControlledDict(collections.UserDict, ABC): +class frozendict(dict): """ - A base dictionary class with configurable mutability. - - Attributes: - _allow_add (bool): Whether new keys can be added. - _allow_del (bool): Whether existing keys can be deleted. - _allow_update (bool): Whether existing keys can be updated. - - Configurable Operations: - This class allows controlling the following dict operations (refer to - https://docs.python.org/3.13/library/stdtypes.html#mapping-types-dict for details): - - - Adding or updating items: - - setter method: `__setitem__` - - `setdefault` - - `update` - - - Deleting items: - - `del dict[key]` - - `pop(key)` - - `popitem` - - `clear()` + A dictionary that does not permit changes. The naming + violates PEP8 to be consistent with standard Python's "frozenset" naming. """ - _allow_add: bool = True - _allow_del: bool = True - _allow_update: bool = True - def __init__(self, *args, **kwargs) -> None: - """Temporarily allow add during initialization.""" - original_allow_add = self._allow_add - - try: - self._allow_add = True - super().__init__(*args, **kwargs) - finally: - self._allow_add = original_allow_add - - # Override add/update operations - def __setitem__(self, key, value) -> None: - """Forbid adding or updating keys based on _allow_add and _allow_update.""" - if key not in self.data and not self._allow_add: - raise TypeError(f"Cannot add new key {key!r}, because add is disabled.") - elif key in self.data and not self._allow_update: - raise TypeError(f"Cannot update key {key!r}, because update is disabled.") + """ + Args: + args: Passthrough arguments for standard dict. + kwargs: Passthrough keyword arguments for standard dict. + """ + dict.__init__(self, *args, **kwargs) - super().__setitem__(key, value) + def __setitem__(self, key: Any, val: Any) -> None: + raise KeyError(f"Cannot overwrite existing key: {str(key)}") def update(self, *args, **kwargs) -> None: - """Forbid adding or updating keys based on _allow_add and _allow_update.""" - for key in dict(*args, **kwargs): - if key not in self.data and not self._allow_add: - raise TypeError( - f"Cannot add new key {key!r} using update, because add is disabled." - ) - elif key in self.data and not self._allow_update: - raise TypeError( - f"Cannot update key {key!r} using update, because update is disabled." - ) - - super().update(*args, **kwargs) - - def setdefault(self, key, default=None) -> Any: - """Forbid adding or updating keys based on _allow_add and _allow_update. - - Note: if not _allow_update, this method would NOT check whether the - new default value is the same as current value for efficiency. """ - if key not in self.data: - if not self._allow_add: - raise TypeError( - f"Cannot add new key using setdefault: {key!r}, because add is disabled." - ) - elif not self._allow_update: - raise TypeError( - f"Cannot update key using setdefault: {key!r}, because update is disabled." - ) - - return super().setdefault(key, default) - - # Override delete operations - def __delitem__(self, key) -> None: - """Forbid deleting keys when self._allow_del is False.""" - if not self._allow_del: - raise TypeError(f"Cannot delete key {key!r}, because delete is disabled.") - super().__delitem__(key) - - def pop(self, key, *args): - """Forbid popping keys when self._allow_del is False.""" - if not self._allow_del: - raise TypeError(f"Cannot pop key {key!r}, because delete is disabled.") - return super().pop(key, *args) - - def popitem(self): - """Forbid popping the last item when self._allow_del is False.""" - if not self._allow_del: - raise TypeError("Cannot pop item, because delete is disabled.") - return super().popitem() - - def clear(self) -> None: - """Forbid clearing the dictionary when self._allow_del is False.""" - if not self._allow_del: - raise TypeError("Cannot clear dictionary, because delete is disabled.") - super().clear() - - -class frozendict(ControlledDict): - """ - A dictionary that does not permit changes. The naming - violates PEP 8 to be consistent with the built-in `frozenset` naming. - """ + Args: + args: Passthrough arguments for standard dict. + kwargs: Passthrough keyword arguments for standard dict. + """ + raise KeyError(f"Cannot update a {self.__class__.__name__}") - _allow_add: bool = False - _allow_del: bool = False - _allow_update: bool = False +class Namespace(dict): + """A dictionary that does not permit to redefine its keys.""" -class Namespace(ControlledDict): - """A dictionary that does not permit update/delete its values (but allows add).""" + def __init__(self, *args, **kwargs) -> None: + """ + Args: + args: Passthrough arguments for standard dict. + kwargs: Passthrough keyword arguments for standard dict. + """ + self.update(*args, **kwargs) - _allow_add: bool = True - _allow_del: bool = False - _allow_update: bool = False + def __setitem__(self, key: Any, val: Any) -> None: + if key in self: + raise KeyError(f"Cannot overwrite existent key: {key!s}") + + dict.__setitem__(self, key, val) + + def update(self, *args, **kwargs) -> None: + """ + Args: + args: Passthrough arguments for standard dict. + kwargs: Passthrough keyword arguments for standard dict. + """ + for k, v in dict(*args, **kwargs).items(): + self[k] = v class AttrDict(dict): """ - Allow accessing values as `dct.key` in addition to the traditional way `dct["key"]`. + Allows to access dict keys as obj.foo in addition + to the traditional way obj['foo']" Examples: - >>> dct = AttrDict(foo=1, bar=2) - >>> assert dct["foo"] is dct.foo - >>> dct.bar = "hello" - - Warnings: - When shadowing dict methods, e.g.: - >>> dct = AttrDict(update="value") - >>> dct.update() # TypeError (the `update` method is overwritten) - - References: - https://stackoverflow.com/a/14620633/24021108 + >>> d = AttrDict(foo=1, bar=2) + >>> assert d["foo"] == d.foo + >>> d.bar = "hello" + >>> assert d.bar == "hello" """ def __init__(self, *args, **kwargs) -> None: - super(AttrDict, self).__init__(*args, **kwargs) + """ + Args: + args: Passthrough arguments for standard dict. + kwargs: Passthrough keyword arguments for standard dict. + """ + super().__init__(*args, **kwargs) self.__dict__ = self - def __setitem__(self, key, value) -> None: - """Check if the key shadows dict method.""" - if key in dir(dict): - warnings.warn( - f"'{key=}' shadows dict method. This may lead to unexpected behavior.", - UserWarning, - stacklevel=2, - ) - super().__setitem__(key, value) + def copy(self) -> Self: + """ + Returns: + Copy of AttrDict + """ + newd = super().copy() + return self.__class__(**newd) class FrozenAttrDict(frozendict): """ A dictionary that: - - Does not permit changes (add/update/delete). - - Allows accessing values as `dct.key` in addition to - the traditional way `dct["key"]`. + * does not permit changes. + * Allows to access dict keys as obj.foo in addition + to the traditional way obj['foo'] """ def __init__(self, *args, **kwargs) -> None: - """Allow add during init, as __setattr__ is called unlike frozendict.""" - self._allow_add = True + """ + Args: + args: Passthrough arguments for standard dict. + kwargs: Passthrough keyword arguments for standard dict. + """ super().__init__(*args, **kwargs) - self._allow_add = False def __getattribute__(self, name: str) -> Any: try: - return object.__getattribute__(self, name) + return super().__getattribute__(name) except AttributeError: - return self[name] + try: + return self[name] + except KeyError as exc: + raise AttributeError(str(exc)) def __setattr__(self, name: str, value: Any) -> None: - if not self._allow_add and name != "_allow_add": - raise TypeError( - f"{self.__class__.__name__} does not support item assignment." - ) - super().__setattr__(name, value) - - def __delattr__(self, name: str) -> None: - raise TypeError(f"{self.__class__.__name__} does not support item deletion.") + raise KeyError( + f"You cannot modify attribute {name} of {self.__class__.__name__}" + ) class MongoDict: @@ -237,11 +151,11 @@ class MongoDict: a nested dict interactively (e.g. documents extracted from a MongoDB database). - >>> m_dct = MongoDict({"a": {"b": 1}, "x": 2}) - >>> assert m_dct.a.b == 1 and m_dct.x == 2 - >>> assert "a" in m_dct and "b" in m_dct.a - >>> m_dct["a"] - {"b": 1} + >>> m = MongoDict({'a': {'b': 1}, 'x': 2}) + >>> assert m.a.b == 1 and m.x == 2 + >>> assert "a" in m and "b" in m.a + >>> m["a"] + {'b': 1} Notes: Cannot inherit from ABC collections.Mapping because otherwise @@ -272,6 +186,7 @@ def __getattribute__(self, name: str) -> Any: try: return super().__getattribute__(name) except AttributeError: + # raise try: a = self._mongo_dict_[name] if isinstance(a, collections.abc.Mapping): @@ -299,25 +214,26 @@ def __dir__(self) -> list: def dict2namedtuple(*args, **kwargs) -> tuple: """ - Helper function to create a `namedtuple` from a dictionary. + Helper function to create a class `namedtuple` from a dictionary. Examples: - >>> tpl = dict2namedtuple(foo=1, bar="hello") - >>> assert tpl.foo == 1 and tpl.bar == "hello" + >>> t = dict2namedtuple(foo=1, bar="hello") + >>> assert t.foo == 1 and t.bar == "hello" - >>> tpl = dict2namedtuple([("foo", 1), ("bar", "hello")]) - >>> assert tpl[0] is tpl.foo and tpl[1] is tpl.bar + >>> t = dict2namedtuple([("foo", 1), ("bar", "hello")]) + >>> assert t[0] == t.foo and t[1] == t.bar Warnings: - The order of the items in the namedtuple is not deterministic if - `kwargs` are used. namedtuples, however, should always be accessed - by attribute hence this behaviour should not be a serious problem. + kwargs are used. + namedtuples, however, should always be accessed by attribute hence + this behaviour should not represent a serious problem. - - Don't use this function in code where memory and performance are - crucial, since a dict is needed to instantiate the tuple! + - Don't use this function in code in which memory and performance are + crucial since a dict is needed to instantiate the tuple! """ - dct = collections.OrderedDict(*args) - dct.update(**kwargs) + d = collections.OrderedDict(*args) + d.update(**kwargs) return collections.namedtuple( - typename="dict2namedtuple", field_names=list(dct.keys()) - )(**dct) + typename="dict2namedtuple", field_names=list(d.keys()) + )(**d) diff --git a/src/monty/design_patterns.py b/src/monty/design_patterns.py index 074505c9a..a99a8a6b7 100644 --- a/src/monty/design_patterns.py +++ b/src/monty/design_patterns.py @@ -7,12 +7,9 @@ import inspect import os from functools import wraps -from typing import TYPE_CHECKING, TypeVar +from typing import Any, Dict, Hashable, Tuple, TypeVar from weakref import WeakValueDictionary -if TYPE_CHECKING: - from typing import Any - def singleton(cls): """ @@ -98,7 +95,7 @@ def new_init(self: Any, *args: Any, **kwargs: Any) -> None: orig_init(self, *args, **kwargs) self._initialized = True - def reduce(self: Any) -> tuple[type, tuple, dict[str, Any]]: + def reduce(self: Any) -> Tuple[type, Tuple, Dict[str, Any]]: for key, value in cache.items(): if value is self: cls, args = key diff --git a/src/monty/fractions.py b/src/monty/fractions.py index f0650e5a5..d29b79600 100644 --- a/src/monty/fractions.py +++ b/src/monty/fractions.py @@ -5,10 +5,7 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Sequence +from typing import Sequence def gcd(*numbers: int) -> int: diff --git a/src/monty/functools.py b/src/monty/functools.py index 969f0d506..32e55517c 100644 --- a/src/monty/functools.py +++ b/src/monty/functools.py @@ -9,11 +9,11 @@ import signal import sys import tempfile +from collections import namedtuple from functools import partial, wraps -from typing import TYPE_CHECKING +from typing import Any, Callable, Union -if TYPE_CHECKING: - from typing import Any, Callable, Union +_CacheInfo = namedtuple("_CacheInfo", ["hits", "misses", "maxsize", "currsize"]) class _HashedSeq(list): # pylint: disable=C0205 diff --git a/src/monty/io.py b/src/monty/io.py index 135bec899..9ca0f2cb9 100644 --- a/src/monty/io.py +++ b/src/monty/io.py @@ -14,102 +14,40 @@ import os import subprocess import time -import warnings from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import IO, Any, Generator, Union + from typing import IO, Generator, Union -class EncodingWarning(Warning): ... # Added in Python 3.10 - - -def zopen( - filename: Union[str, Path], - /, - mode: str | None = None, - **kwargs: Any, -) -> IO | bz2.BZ2File | gzip.GzipFile | lzma.LZMAFile: +def zopen(filename: Union[str, Path], *args, **kwargs) -> IO: """ - 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", ...)`. - - 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. + This function wraps around the bz2, gzip, lzma, xz and standard Python's open + function to deal intelligently with bzipped, gzipped or standard text + files. Args: - filename (str | Path): The file to open. - mode (str): The mode in which the file is opened, you MUST - explicitly specify "b" for binary or "t" for text. - **kwargs: Additional keyword arguments to pass to `open`. + filename (str/Path): filename or pathlib.Path. + *args: Standard args for Python open(..). E.g., 'r' for read, 'w' for + write. + **kwargs: Standard kwargs for Python open(..). Returns: - TextIO | BinaryIO | bz2.BZ2File | gzip.GzipFile | lzma.LZMAFile + File-like object. Supports with context. """ - # Deadline for dropping implicit `mode` support - _deadline: str = "2025-06-01" - - # Warn against default `mode` - # TODO: remove default value of `mode` to force user to give one after deadline - if mode is None: - warnings.warn( - "We strongly discourage using a default `mode`, it would be" - f"set to `r` now but would not be allowed after {_deadline}", - FutureWarning, - stacklevel=2, - ) - mode = "r" - - # Warn against implicit text/binary `mode` - # TODO: replace warning with exception after deadline - elif not ("b" in mode or "t" in mode): - warnings.warn( - "We strongly discourage using implicit binary/text `mode`, " - f"and this would not be allowed after {_deadline}. " - "I.e. you should pass t/b in `mode`.", - FutureWarning, - stacklevel=2, - ) - - # Warn against default `encoding` in text mode - 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, - ) - kwargs["encoding"] = "utf-8" + if filename is not None and isinstance(filename, Path): + filename = str(filename) _name, ext = os.path.splitext(filename) - ext = ext.lower() - - if ext == ".bz2": - return bz2.open(filename, mode, **kwargs) - if ext == ".gz": - return gzip.open(filename, mode, **kwargs) - if ext == ".z": - # TODO: drop ".z" extension support after 2026-01-01 - warnings.warn( - "Python gzip is not able to (de)compress LZW-compressed files. " - "You should rename it to .gz. Support for the '.z' extension will " - "be removed after 2026-01-01.", - category=FutureWarning, - stacklevel=2, - ) - return gzip.open(filename, mode, **kwargs) - if ext in {".xz", ".lzma"}: - return lzma.open(filename, mode, **kwargs) - - return open(filename, mode, **kwargs) + ext = ext.upper() + if ext == ".BZ2": + return bz2.open(filename, *args, **kwargs) + if ext in {".GZ", ".Z"}: + return gzip.open(filename, *args, **kwargs) + if ext in {".XZ", ".LZMA"}: + return lzma.open(filename, *args, **kwargs) + return open(filename, *args, **kwargs) def reverse_readfile(filename: Union[str, Path]) -> Generator[str, str, None]: diff --git a/src/monty/json.py b/src/monty/json.py index 973490f2e..d0626d3e3 100644 --- a/src/monty/json.py +++ b/src/monty/json.py @@ -29,10 +29,8 @@ try: import bson - from bson import json_util except ImportError: bson = None - json_util = None try: import orjson @@ -45,7 +43,7 @@ def _load_redirect(redirect_file) -> dict: try: - with open(redirect_file, encoding="utf-8") as f: + with open(redirect_file) as f: yaml = YAML() d = yaml.load(f) except OSError: @@ -448,7 +446,7 @@ class is not entirely MSONable. raise FileExistsError(f"strict is true and file {pickle_path} exists") # Save the json file - with open(json_path, "w", encoding="utf-8") as outfile: + with open(json_path, "w") as outfile: outfile.write(encoded) # Save the pickle file if we have anything to save from the name_object_map @@ -501,7 +499,7 @@ def _d_from_path(file_path): save_dir = json_path.parent pickle_path = save_dir / f"{json_path.stem}.pkl" - with open(json_path, "r", encoding="utf-8") as infile: + with open(json_path, "r") as infile: d = json.loads(infile.read()) if pickle_path.exists(): @@ -864,11 +862,7 @@ def decode(self, s): :param s: string :return: Object. """ - if bson is not None: - # need to pass `json_options` to ensure that datetimes are not - # converted by BSON - d = json_util.loads(s, json_options=json_util.JSONOptions(tz_aware=True)) - elif orjson is not None: + if orjson is not None: try: d = orjson.loads(s) except orjson.JSONDecodeError: @@ -998,13 +992,7 @@ def jsanitize( if recursive_msonable: try: - return jsanitize( - obj.as_dict(), - strict=strict, - allow_bson=allow_bson, - enum_values=enum_values, - recursive_msonable=recursive_msonable, - ) + return obj.as_dict() except AttributeError: pass diff --git a/src/monty/re.py b/src/monty/re.py index 7fddbd72f..0dfb75a33 100644 --- a/src/monty/re.py +++ b/src/monty/re.py @@ -46,11 +46,7 @@ def regrep( """ compiled = {k: re.compile(v) for k, v in patterns.items()} matches = collections.defaultdict(list) - gen = ( - reverse_readfile(filename) - if reverse - else zopen(filename, "rt", encoding="utf-8") - ) + gen = reverse_readfile(filename) if reverse else zopen(filename, "rt") for i, line in enumerate(gen): for k, p in compiled.items(): if m := p.search(line): diff --git a/src/monty/serialization.py b/src/monty/serialization.py index 13f3fb004..6732a674a 100644 --- a/src/monty/serialization.py +++ b/src/monty/serialization.py @@ -7,7 +7,7 @@ import json import os -from typing import TYPE_CHECKING, TextIO, cast +from typing import TYPE_CHECKING from ruamel.yaml import YAML @@ -22,7 +22,7 @@ if TYPE_CHECKING: from pathlib import Path - from typing import Any, Optional, TextIO, Union + from typing import Any, Optional, Union def loadfn(fn: Union[str, Path], *args, fmt: Optional[str] = None, **kwargs) -> Any: @@ -67,7 +67,7 @@ def loadfn(fn: Union[str, Path], *args, fmt: Optional[str] = None, **kwargs) -> with zopen(fn, "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, "rt") as fp: if fmt == "yaml": if YAML is None: raise RuntimeError("Loading of YAML files requires ruamel.yaml.") @@ -120,9 +120,7 @@ def dumpfn(obj: object, fn: Union[str, Path], *args, fmt=None, **kwargs) -> None with zopen(fn, "wb") as fp: msgpack.dump(obj, fp, *args, **kwargs) # pylint: disable=E1101 else: - with zopen(fn, "wt", encoding="utf-8") as fp: - fp = cast(TextIO, fp) - + with zopen(fn, "wt") as fp: if fmt == "yaml": if YAML is None: raise RuntimeError("Loading of YAML files requires ruamel.yaml.") diff --git a/tasks.py b/tasks.py index fdcaac6bd..7432034a0 100755 --- a/tasks.py +++ b/tasks.py @@ -39,7 +39,7 @@ def make_doc(ctx: Context) -> None: ctx.run("rm *.rst", warn=True) ctx.run("cp markdown/monty*.md .") for fn in glob.glob("monty*.md"): - with open(fn, encoding="utf-8") as f: + with open(fn) as f: lines = [line.rstrip() for line in f if "Submodules" not in line] if fn == "monty.md": preamble = [ @@ -59,7 +59,7 @@ def make_doc(ctx: Context) -> None: "---", "", ] - with open(fn, "w", encoding="utf-8") as f: + with open(fn, "w") as f: f.write("\n".join(preamble + lines)) ctx.run("rm -r markdown", warn=True) @@ -67,9 +67,9 @@ def make_doc(ctx: Context) -> None: ctx.run("mv README.md index.md") ctx.run("rm -rf *.orig doctrees", warn=True) - with open("index.md", encoding="utf-8") as f: + with open("index.md") as f: contents = f.read() - with open("index.md", "w", encoding="utf-8") as f: + with open("index.md", "w") as f: contents = re.sub( r"\n## Official Documentation[^#]*", "{: .no_toc }\n\n## Table of contents\n{: .no_toc .text-delta }\n* TOC\n{:toc}\n\n", @@ -104,7 +104,7 @@ def setver(ctx: Context) -> None: @task def release_github(ctx: Context) -> None: - with open("docs/changelog.md", encoding="utf-8") as f: + with open("docs/changelog.md") as f: contents = f.read() toks = re.split("##", contents) desc = toks[1].strip() diff --git a/tests/test_collections.py b/tests/test_collections.py index 118a08a87..58d2af9e7 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -1,262 +1,55 @@ from __future__ import annotations -from collections import UserDict +import os import pytest -from monty.collections import ( - AttrDict, - ControlledDict, - FrozenAttrDict, - MongoDict, - Namespace, - dict2namedtuple, - frozendict, - tree, -) - - -def test_tree(): - x = tree() - x["a"]["b"]["c"]["d"] = 1 - assert "b" in x["a"] - assert "c" not in x["a"] - assert "c" in x["a"]["b"] - assert x["a"]["b"]["c"]["d"] == 1 - - -class TestControlledDict: - def test_add_allowed(self): - dct = ControlledDict(a=1) - dct._allow_add = True - - dct["b"] = 2 - assert dct["b"] == 2 - - dct.update(d=3) - assert dct["d"] == 3 - - dct.setdefault("e", 4) - assert dct["e"] == 4 - - def test_add_disabled(self): - dct = ControlledDict(a=1) - dct._allow_add = False - - with pytest.raises(TypeError, match="add is disabled"): - dct["b"] = 2 - - with pytest.raises(TypeError, match="add is disabled"): - dct.update(b=2) - - with pytest.raises(TypeError, match="add is disabled"): - dct.setdefault("c", 2) - - def test_update_allowed(self): - dct = ControlledDict(a=1) - dct._allow_update = True - - dct["a"] = 2 - assert dct["a"] == 2 - - dct.update({"a": 3}) - assert dct["a"] == 3 - - dct.setdefault("a", 4) # existing key - assert dct["a"] == 3 - - def test_update_disabled(self): - dct = ControlledDict(a=1) - dct._allow_update = False - - with pytest.raises(TypeError, match="update is disabled"): - dct["a"] = 2 - - with pytest.raises(TypeError, match="update is disabled"): - dct.update({"a": 3}) - - with pytest.raises(TypeError, match="update is disabled"): - dct.setdefault("a", 4) - - def test_del_allowed(self): - dct = ControlledDict(a=1, b=2, c=3, d=4) - dct._allow_del = True - - del dct["a"] - assert "a" not in dct - - val = dct.pop("b") - assert val == 2 and "b" not in dct - - val = dct.popitem() - assert val == ("c", 3) and "c" not in dct - - dct.clear() - assert dct == {} - - def test_del_disabled(self): - dct = ControlledDict(a=1) - dct._allow_del = False - - with pytest.raises(TypeError, match="delete is disabled"): - del dct["a"] - - with pytest.raises(TypeError, match="delete is disabled"): - dct.pop("a") - - with pytest.raises(TypeError, match="delete is disabled"): - dct.popitem() - - with pytest.raises(TypeError, match="delete is disabled"): - dct.clear() - - def test_frozen_like(self): - """Make sure add and update are allowed at init time.""" - ControlledDict._allow_add = False - ControlledDict._allow_update = False - - dct = ControlledDict({"hello": "world"}) - assert isinstance(dct, UserDict) - assert dct["hello"] == "world" - - assert not dct._allow_add - assert not dct._allow_update - - -def test_frozendict(): - dct = frozendict({"hello": "world"}) - assert isinstance(dct, UserDict) - assert dct["hello"] == "world" - - assert not dct._allow_add - assert not dct._allow_update - assert not dct._allow_del - - # Test setter - with pytest.raises(TypeError, match="add is disabled"): - dct["key"] = "val" - - # Test update - with pytest.raises(TypeError, match="add is disabled"): - dct.update(key="val") - - # Test pop - with pytest.raises(TypeError, match="delete is disabled"): - dct.pop("key") - - # Test delete - with pytest.raises(TypeError, match="delete is disabled"): - del dct["key"] - - -def test_namespace_dict(): - dct = Namespace(key="val") - assert isinstance(dct, UserDict) - - # Test setter - dct["hello"] = "world" - assert dct["key"] == "val" - - # Test update (not allowed) - with pytest.raises(TypeError, match="update is disabled"): - dct["key"] = "val" - - with pytest.raises(TypeError, match="update is disabled"): - dct.update({"key": "val"}) - - # Test delete (not allowed) - with pytest.raises(TypeError, match="delete is disabled"): - del dct["key"] - - -def test_attr_dict(): - dct = AttrDict(foo=1, bar=2) - - # Test get attribute - assert dct.bar == 2 - assert dct["foo"] is dct.foo - - # Test key not found error - with pytest.raises(KeyError, match="no-such-key"): - dct["no-such-key"] - - # Test setter - dct.bar = "hello" - assert dct["bar"] == "hello" - - # Test delete - del dct.bar - assert "bar" not in dct - - # Test builtin dict method shadowing - with pytest.warns(UserWarning, match="shadows dict method"): - dct["update"] = "value" - - -def test_frozen_attrdict(): - dct = FrozenAttrDict({"hello": "world", 1: 2}) - assert isinstance(dct, UserDict) - - # Test attribute-like operations - with pytest.raises(TypeError, match="does not support item assignment"): - dct.foo = "bar" - - with pytest.raises(TypeError, match="does not support item assignment"): - dct.hello = "new" - - with pytest.raises(TypeError, match="does not support item deletion"): - del dct.hello - - # Test get value - assert dct["hello"] == "world" - assert dct.hello == "world" - assert dct["hello"] is dct.hello # identity check - - # Test adding item - with pytest.raises(TypeError, match="add is disabled"): - dct["foo"] = "bar" - - # Test modifying existing item - with pytest.raises(TypeError, match="update is disabled"): - dct["hello"] = "new" - - # Test update - with pytest.raises(TypeError, match="update is disabled"): - dct.update({"hello": "world"}) - - # Test pop - with pytest.raises(TypeError, match="delete is disabled"): - dct.pop("hello") - - with pytest.raises(TypeError, match="delete is disabled"): - dct.popitem() - - # Test delete - with pytest.raises(TypeError, match="delete is disabled"): - del dct["hello"] - - with pytest.raises(TypeError, match="delete is disabled"): - dct.clear() - - -def test_mongo_dict(): - m_dct = MongoDict({"a": {"b": 1}, "x": 2}) - assert m_dct.a.b == 1 - assert m_dct.x == 2 - assert "a" in m_dct - assert "b" in m_dct.a - assert m_dct["a"] == {"b": 1} - - -def test_dict2namedtuple(): - # Init from dict - tpl = dict2namedtuple(foo=1, bar="hello") - assert isinstance(tpl, tuple) - assert tpl.foo == 1 and tpl.bar == "hello" - - # Init from list of tuples - tpl = dict2namedtuple([("foo", 1), ("bar", "hello")]) - assert isinstance(tpl, tuple) - assert tpl[0] == 1 - assert tpl[1] == "hello" - assert tpl[0] is tpl.foo and tpl[1] is tpl.bar +from monty.collections import AttrDict, FrozenAttrDict, Namespace, frozendict, tree + +TEST_DIR = os.path.join(os.path.dirname(__file__), "test_files") + + +class TestFrozenDict: + def test_frozen_dict(self): + d = frozendict({"hello": "world"}) + with pytest.raises(KeyError): + d["k"] == "v" + assert d["hello"] == "world" + + def test_namespace_dict(self): + d = Namespace(foo="bar") + d["hello"] = "world" + assert d["foo"] == "bar" + with pytest.raises(KeyError): + d.update({"foo": "spam"}) + + def test_attr_dict(self): + d = AttrDict(foo=1, bar=2) + assert d.bar == 2 + assert d["foo"] == d.foo + d.bar = "hello" + assert d["bar"] == "hello" + + def test_frozen_attrdict(self): + d = FrozenAttrDict({"hello": "world", 1: 2}) + assert d["hello"] == "world" + assert d.hello == "world" + with pytest.raises(KeyError): + d["updating"] == 2 + + with pytest.raises(KeyError): + d["foo"] = "bar" + with pytest.raises(KeyError): + d.foo = "bar" + with pytest.raises(KeyError): + d.hello = "new" + + +class TestTree: + def test_tree(self): + x = tree() + x["a"]["b"]["c"]["d"] = 1 + assert "b" in x["a"] + assert "c" not in x["a"] + assert "c" in x["a"]["b"] + assert x["a"]["b"]["c"]["d"] == 1 diff --git a/tests/test_files/myfile b/tests/test_files/myfile new file mode 100644 index 000000000..1e35ee2e0 --- /dev/null +++ b/tests/test_files/myfile @@ -0,0 +1,2 @@ +HelloWorld. + diff --git a/tests/test_files/myfile.gz b/tests/test_files/myfile.gz new file mode 100644 index 000000000..c57dc4e14 Binary files /dev/null and b/tests/test_files/myfile.gz differ diff --git a/tests/test_files/myfile_bz2.bz2.gz b/tests/test_files/myfile_bz2.bz2.gz new file mode 100644 index 000000000..80626091b Binary files /dev/null and b/tests/test_files/myfile_bz2.bz2.gz differ diff --git a/tests/test_files/myfile_gz.gz b/tests/test_files/myfile_gz.gz new file mode 100644 index 000000000..e23fe3b2f Binary files /dev/null and b/tests/test_files/myfile_gz.gz differ diff --git a/tests/test_files/myfile_lzma.lzma b/tests/test_files/myfile_lzma.lzma new file mode 100644 index 000000000..efcaa021b Binary files /dev/null and b/tests/test_files/myfile_lzma.lzma differ diff --git a/tests/test_files/myfile_lzma.lzma.gz b/tests/test_files/myfile_lzma.lzma.gz new file mode 100644 index 000000000..e01bafdc7 Binary files /dev/null and b/tests/test_files/myfile_lzma.lzma.gz differ diff --git a/tests/test_files/myfile_txt b/tests/test_files/myfile_txt new file mode 100644 index 000000000..1e35ee2e0 --- /dev/null +++ b/tests/test_files/myfile_txt @@ -0,0 +1,2 @@ +HelloWorld. + diff --git a/tests/test_files/myfile_txt.gz b/tests/test_files/myfile_txt.gz new file mode 100644 index 000000000..467b49633 Binary files /dev/null and b/tests/test_files/myfile_txt.gz differ diff --git a/tests/test_files/myfile_xz.xz b/tests/test_files/myfile_xz.xz new file mode 100644 index 000000000..af2cccd77 Binary files /dev/null and b/tests/test_files/myfile_xz.xz differ diff --git a/tests/test_files/myfile_xz.xz.gz b/tests/test_files/myfile_xz.xz.gz new file mode 100644 index 000000000..9fd186e8d Binary files /dev/null and b/tests/test_files/myfile_xz.xz.gz differ diff --git a/tests/test_files/real_lzw_file.txt.Z b/tests/test_files/real_lzw_file.txt.Z deleted file mode 100644 index c36628550..000000000 --- a/tests/test_files/real_lzw_file.txt.Z +++ /dev/null @@ -1 +0,0 @@ -hʰaF \ No newline at end of file diff --git a/tests/test_io.py b/tests/test_io.py index 10ad865ba..052bc471b 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,15 +1,12 @@ from __future__ import annotations -import gzip import os -import warnings from pathlib import Path from unittest.mock import patch import pytest from monty.io import ( - EncodingWarning, FileLock, FileLockException, reverse_readfile, @@ -30,7 +27,7 @@ def test_reverse_readline(self): order, i.e. the first line that is read corresponds to the last line. number """ - with open(os.path.join(TEST_DIR, "3000_lines.txt"), encoding="utf-8") as f: + with open(os.path.join(TEST_DIR, "3000_lines.txt")) as f: for idx, line in enumerate(reverse_readline(f)): assert ( int(line) == self.NUMLINES - idx @@ -40,7 +37,7 @@ def test_reverse_readline_fake_big(self): """ Make sure that large text files are read properly. """ - with open(os.path.join(TEST_DIR, "3000_lines.txt"), encoding="utf-8") as f: + with open(os.path.join(TEST_DIR, "3000_lines.txt")) as f: for idx, line in enumerate(reverse_readline(f, max_mem=0)): assert ( int(line) == self.NUMLINES - idx @@ -62,7 +59,7 @@ def test_empty_file(self): Make sure an empty file does not throw an error when reverse_readline is called, which was a problem with an earlier implementation. """ - with open(os.path.join(TEST_DIR, "empty_file.txt"), encoding="utf-8") as f: + with open(os.path.join(TEST_DIR, "empty_file.txt")) as f: for _line in reverse_readline(f): raise ValueError("an empty file is being read!") @@ -76,12 +73,10 @@ def test_line_ending(self): assert linux_line_end == "\n" with ScratchDir("./test_files"): - with open( - "sample_unix_mac.txt", "w", newline=linux_line_end, encoding="utf-8" - ) as file: + with open("sample_unix_mac.txt", "w", newline=linux_line_end) as file: file.write(linux_line_end.join(contents)) - with open("sample_unix_mac.txt", encoding="utf-8") as file: + with open("sample_unix_mac.txt") as file: for idx, line in enumerate(reverse_readfile(file)): assert line == contents[len(contents) - idx - 1] @@ -91,15 +86,10 @@ def test_line_ending(self): assert windows_line_end == "\r\n" with ScratchDir("./test_files"): - with open( - "sample_windows.txt", - "w", - newline=windows_line_end, - encoding="utf-8", - ) as file: + with open("sample_windows.txt", "w", newline=windows_line_end) as file: file.write(windows_line_end.join(contents)) - with open("sample_windows.txt", encoding="utf-8") as file: + with open("sample_windows.txt") as file: for idx, line in enumerate(reverse_readfile(file)): assert line == contents[len(contents) - idx - 1] @@ -152,9 +142,7 @@ def test_line_ending(self): assert linux_line_end == "\n" with ScratchDir("./test_files"): - with open( - "sample_unix_mac.txt", "w", newline=linux_line_end, encoding="utf-8" - ) as file: + with open("sample_unix_mac.txt", "w", newline=linux_line_end) as file: file.write(linux_line_end.join(contents)) for idx, line in enumerate(reverse_readfile("sample_unix_mac.txt")): @@ -166,12 +154,7 @@ def test_line_ending(self): assert windows_line_end == "\r\n" with ScratchDir("./test_files"): - with open( - "sample_windows.txt", - "w", - newline=windows_line_end, - encoding="utf-8", - ) as file: + with open("sample_windows.txt", "w", newline=windows_line_end) as file: file.write(windows_line_end.join(contents)) for idx, line in enumerate(reverse_readfile("sample_windows.txt")): @@ -179,102 +162,25 @@ def test_line_ending(self): class TestZopen: - @pytest.mark.parametrize("extension", [".txt", ".bz2", ".gz", ".xz", ".lzma"]) - def test_read_write_files(self, extension): - """Test read/write in binary/text mode: - - uncompressed text file: .txt - - compressed files: bz2/gz/xz/lzma - """ - filename = f"test_file{extension}" - content = "This is a test file.\n" - - with ScratchDir("."): - # Test write and read in text mode - with zopen(filename, "wt", encoding="utf-8") as f: - f.write(content) - - with zopen(Path(filename), "rt", encoding="utf-8") as f: - assert f.read() == content - - # Test write and read in binary mode - with zopen(Path(filename), "wb") as f: - f.write(content.encode()) - - with zopen(filename, "rb") as f: - assert f.read() == content.encode() - - def test_lzw_files(self): - """gzip is not really able to (de)compress LZW files. - - TODO: remove text file real_lzw_file.txt.Z after dropping - ".Z" extension support - """ - # Test a fake LZW file (just with .Z extension but DEFLATED algorithm) - filename = "test.Z" - content = "This is not a real LZW compressed file.\n" - - with ( - ScratchDir("."), - pytest.warns(FutureWarning, match="compress LZW-compressed files"), - ): - # Test write and read in text mode - with zopen(filename, "wt", encoding="utf-8") as f: - f.write(content) - - with zopen(filename, "rt", encoding="utf-8") as f: - assert f.read() == content - - # Test write and read in binary mode - with zopen(filename, "wb") as f: - f.write(content.encode()) - - with zopen(filename, "rb") as f: - assert f.read() == content.encode() - - # Cannot decompress a real LZW file - with ( - pytest.raises(gzip.BadGzipFile, match="Not a gzipped file"), - zopen(f"{TEST_DIR}/real_lzw_file.txt.Z", "rt", encoding="utf-8") as f, - ): - f.read() - - @pytest.mark.parametrize("extension", [".txt", ".bz2", ".gz", ".xz", ".lzma"]) - def test_warnings(self, extension): - filename = f"test_warning{extension}" - content = "Test warning" - - with ScratchDir("."): - # Default `encoding` warning - with ( - pytest.warns(EncodingWarning, match="use UTF-8 by default"), - zopen(filename, "wt") as f, - ): - f.write(content) - - # Implicit text/binary `mode` warning - warnings.filterwarnings( - "ignore", category=EncodingWarning, message="argument not specified" - ) - with ( - pytest.warns( - FutureWarning, match="discourage using implicit binary/text" - ), - zopen(filename, "r") as f, - ): - if extension == ".txt": - assert f.readline() == content - else: - assert f.readline().decode("utf-8") == content - - # Implicit `mode` warning - with ( - pytest.warns(FutureWarning, match="discourage using a default `mode`"), - zopen(filename) as f, - ): - if extension == ".txt": - assert f.readline() == content - else: - assert f.readline().decode("utf-8") == content + def test_zopen(self): + with zopen(os.path.join(TEST_DIR, "myfile_gz.gz"), mode="rt") as f: + assert f.read() == "HelloWorld.\n\n" + with zopen(os.path.join(TEST_DIR, "myfile_bz2.bz2"), mode="rt") as f: + assert f.read() == "HelloWorld.\n\n" + with zopen(os.path.join(TEST_DIR, "myfile_bz2.bz2"), "rt") as f: + assert f.read() == "HelloWorld.\n\n" + with zopen(os.path.join(TEST_DIR, "myfile_xz.xz"), "rt") as f: + assert f.read() == "HelloWorld.\n\n" + with zopen(os.path.join(TEST_DIR, "myfile_lzma.lzma"), "rt") as f: + assert f.read() == "HelloWorld.\n\n" + with zopen(os.path.join(TEST_DIR, "myfile"), mode="rt") as f: + assert f.read() == "HelloWorld.\n\n" + + def test_Path_objects(self): + p = Path(TEST_DIR) / "myfile_gz.gz" + + with zopen(p, mode="rt") as f: + assert f.read() == "HelloWorld.\n\n" class TestFileLock: diff --git a/tests/test_json.py b/tests/test_json.py index 65dac8cd3..a872b7c8c 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -794,18 +794,6 @@ def test_jsanitize(self): assert clean_recursive_msonable["hello"][1] == "test" assert clean_recursive_msonable["test"] == "hi" - DoubleGoodMSONClass = GoodMSONClass(1, 2, 3) - DoubleGoodMSONClass.values = [GoodMSONClass(1, 2, 3)] - clean_recursive_msonable = jsanitize( - DoubleGoodMSONClass, recursive_msonable=True - ) - assert clean_recursive_msonable["a"] == 1 - assert clean_recursive_msonable["b"] == 2 - assert clean_recursive_msonable["c"] == 3 - assert clean_recursive_msonable["values"][0]["a"] == 1 - assert clean_recursive_msonable["values"][0]["b"] == 2 - assert clean_recursive_msonable["values"][0]["c"] == 3 - d = {"dt": datetime.datetime.now()} clean = jsanitize(d) assert isinstance(clean["dt"], str) @@ -1080,31 +1068,3 @@ def test_enum(self): assert d_ == {"v": "value_a"} na2 = EnumAsDict.from_dict(d_) assert na2 == na1 - - @pytest.mark.skipif(ObjectId is None, reason="bson not present") - def test_extended_json(self): - from bson import json_util - - ext_json_dict = { - "datetime": datetime.datetime.now(datetime.timezone.utc), - "NaN": float("NaN"), - "infinity": float("inf"), - "-infinity": -float("inf"), - } - ext_json_str = json_util.dumps(ext_json_dict) - - not_serialized = json.loads(ext_json_str) - assert all(isinstance(v, dict) for v in not_serialized.values()) - - reserialized = MontyDecoder().decode(ext_json_str) - for k, v in ext_json_dict.items(): - if k == "datetime": - # BSON's json_util only saves datetimes up to microseconds - assert reserialized[k].timestamp() == pytest.approx( - v.timestamp(), abs=1e-3 - ) - elif k == "NaN": - assert np.isnan(reserialized[k]) - else: - assert v == reserialized[k] - assert not isinstance(reserialized[k], dict) diff --git a/tests/test_os.py b/tests/test_os.py index f0225f70c..c4119c768 100644 --- a/tests/test_os.py +++ b/tests/test_os.py @@ -88,8 +88,8 @@ def test_makedirs_p(self): makedirs_p(self.test_dir_path) assert os.path.exists(self.test_dir_path) makedirs_p(self.test_dir_path) - with pytest.raises(OSError, match="exists"): - makedirs_p(os.path.join(TEST_DIR, "3000_lines.txt")) + with pytest.raises(OSError): + makedirs_p(os.path.join(TEST_DIR, "myfile_txt")) def teardown_method(self): os.rmdir(self.test_dir_path) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index abe14e2a3..e7c853a58 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -82,6 +82,6 @@ def test_mpk(self): os.chdir("mpk_test") fname = os.path.abspath("test_file.json") dumpfn({"test": 1}, fname) - with open("test_file.json", encoding="utf-8") as f: + with open("test_file.json") as f: reloaded = json.loads(f.read()) assert reloaded["test"] == 1 diff --git a/tests/test_shutil.py b/tests/test_shutil.py index c47d4b462..dfc1375fd 100644 --- a/tests/test_shutil.py +++ b/tests/test_shutil.py @@ -25,15 +25,11 @@ class TestCopyR: def setup_method(self): - os.mkdir(os.path.join(test_dir, "cpr_src")) - with open( - os.path.join(test_dir, "cpr_src", "test"), "w", encoding="utf-8" - ) as f: + os.mkdir(os.path.join(TEST_DIR, "cpr_src")) + with open(os.path.join(TEST_DIR, "cpr_src", "test"), "w") as f: f.write("what") - os.mkdir(os.path.join(test_dir, "cpr_src", "sub")) - with open( - os.path.join(test_dir, "cpr_src", "sub", "testr"), "w", encoding="utf-8" - ) as f: + os.mkdir(os.path.join(TEST_DIR, "cpr_src", "sub")) + with open(os.path.join(TEST_DIR, "cpr_src", "sub", "testr"), "w") as f: f.write("what2") if os.name != "nt": os.symlink( @@ -42,18 +38,18 @@ def setup_method(self): ) def test_recursive_copy_and_compress(self): - copy_r(os.path.join(test_dir, "cpr_src"), os.path.join(test_dir, "cpr_dst")) - assert os.path.exists(os.path.join(test_dir, "cpr_dst", "test")) - assert os.path.exists(os.path.join(test_dir, "cpr_dst", "sub", "testr")) - - compress_dir(os.path.join(test_dir, "cpr_src")) - assert os.path.exists(os.path.join(test_dir, "cpr_src", "test.gz")) - assert os.path.exists(os.path.join(test_dir, "cpr_src", "sub", "testr.gz")) - - decompress_dir(os.path.join(test_dir, "cpr_src")) - assert os.path.exists(os.path.join(test_dir, "cpr_src", "test")) - assert os.path.exists(os.path.join(test_dir, "cpr_src", "sub", "testr")) - with open(os.path.join(test_dir, "cpr_src", "test"), encoding="utf-8") as f: + copy_r(os.path.join(TEST_DIR, "cpr_src"), os.path.join(TEST_DIR, "cpr_dst")) + assert os.path.exists(os.path.join(TEST_DIR, "cpr_dst", "test")) + assert os.path.exists(os.path.join(TEST_DIR, "cpr_dst", "sub", "testr")) + + compress_dir(os.path.join(TEST_DIR, "cpr_src")) + assert os.path.exists(os.path.join(TEST_DIR, "cpr_src", "test.gz")) + assert os.path.exists(os.path.join(TEST_DIR, "cpr_src", "sub", "testr.gz")) + + decompress_dir(os.path.join(TEST_DIR, "cpr_src")) + assert os.path.exists(os.path.join(TEST_DIR, "cpr_src", "test")) + assert os.path.exists(os.path.join(TEST_DIR, "cpr_src", "sub", "testr")) + with open(os.path.join(TEST_DIR, "cpr_src", "test")) as f: txt = f.read() assert txt == "what" @@ -70,7 +66,7 @@ def teardown_method(self): class TestCompressFileDir: def setup_method(self): - with open(os.path.join(test_dir, "tempfile"), "w", encoding="utf-8") as f: + with open(os.path.join(TEST_DIR, "tempfile"), "w") as f: f.write("hello world") def test_compress_and_decompress_file(self): @@ -85,7 +81,7 @@ def test_compress_and_decompress_file(self): assert os.path.exists(fname) assert not os.path.exists(fname + "." + fmt) - with open(fname, encoding="utf-8") as f: + with open(fname) as f: assert f.read() == "hello world" with pytest.raises(ValueError): @@ -117,7 +113,7 @@ def test_compress_and_decompress_with_target_dir(self): shutil.move(decompressed_file_path, fname) shutil.rmtree(target_dir) - with open(fname, encoding="utf-8") as f: + with open(fname) as f: assert f.read() == "hello world" def teardown_method(self): @@ -126,10 +122,8 @@ def teardown_method(self): class TestGzipDir: def setup_method(self): - os.mkdir(os.path.join(test_dir, "gzip_dir")) - with open( - os.path.join(test_dir, "gzip_dir", "tempfile"), "w", encoding="utf-8" - ) as f: + os.mkdir(os.path.join(TEST_DIR, "gzip_dir")) + with open(os.path.join(TEST_DIR, "gzip_dir", "tempfile"), "w") as f: f.write("what") self.mtime = os.path.getmtime(os.path.join(TEST_DIR, "gzip_dir", "tempfile")) @@ -152,7 +146,7 @@ def test_gzip_dir_file_coexist(self): gz_f = f"{full_f}.gz" # Create both the file and its gzipped version - with open(full_f, "w", encoding="utf-8") as f: + with open(full_f, "w") as f: f.write("not gzipped") with GzipFile(gz_f, "wb") as g: g.write(b"gzipped") @@ -163,7 +157,7 @@ def test_gzip_dir_file_coexist(self): gzip_dir(os.path.join(TEST_DIR, "gzip_dir")) # Verify contents of the files - with open(full_f, "r", encoding="utf-8") as f: + with open(full_f, "r") as f: assert f.read() == "not gzipped" with GzipFile(gz_f, "rb") as g: @@ -173,7 +167,7 @@ def test_handle_sub_dirs(self): sub_dir = os.path.join(TEST_DIR, "gzip_dir", "sub_dir") sub_file = os.path.join(sub_dir, "new_tempfile") os.mkdir(sub_dir) - with open(sub_file, "w", encoding="utf-8") as f: + with open(sub_file, "w") as f: f.write("anotherwhat") gzip_dir(os.path.join(TEST_DIR, "gzip_dir")) diff --git a/tests/test_tempfile.py b/tests/test_tempfile.py index eae851891..088aec670 100644 --- a/tests/test_tempfile.py +++ b/tests/test_tempfile.py @@ -19,7 +19,7 @@ def setup_method(self): def test_with_copy(self): # We write a pre-scratch file. - with open("pre_scratch_text", "w", encoding="utf-8") as f: + with open("pre_scratch_text", "w") as f: f.write("write") with ScratchDir( @@ -27,7 +27,7 @@ def test_with_copy(self): copy_from_current_on_enter=True, copy_to_current_on_exit=True, ) as d: - with open("scratch_text", "w", encoding="utf-8") as f: + with open("scratch_text", "w") as f: f.write("write") files = os.listdir(d) assert "scratch_text" in files @@ -49,7 +49,7 @@ def test_with_copy(self): def test_with_copy_gzip(self): # We write a pre-scratch file. - with open("pre_scratch_text", "w", encoding="utf-8") as f: + with open("pre_scratch_text", "w") as f: f.write("write") init_gz_files = [f for f in os.listdir(os.getcwd()) if f.endswith(".gz")] with pytest.warns(match="Both 3000_lines.txt and 3000_lines.txt.gz exist."): @@ -60,7 +60,7 @@ def test_with_copy_gzip(self): copy_to_current_on_exit=True, gzip_on_exit=True, ), - open("scratch_text", "w", encoding="utf-8") as f, + open("scratch_text", "w") as f, ): f.write("write") files = os.listdir(os.getcwd()) @@ -74,7 +74,7 @@ def test_with_copy_gzip(self): def test_with_copy_nodelete(self): # We write a pre-scratch file. - with open("pre_scratch_text", "w", encoding="utf-8") as f: + with open("pre_scratch_text", "w") as f: f.write("write") with ScratchDir( @@ -83,7 +83,7 @@ def test_with_copy_nodelete(self): copy_to_current_on_exit=True, delete_removed_files=False, ) as d: - with open("scratch_text", "w", encoding="utf-8") as f: + with open("scratch_text", "w") as f: f.write("write") files = os.listdir(d) assert "scratch_text" in files @@ -109,7 +109,7 @@ def test_no_copy(self): copy_from_current_on_enter=False, copy_to_current_on_exit=False, ) as d: - with open("scratch_text", "w", encoding="utf-8") as f: + with open("scratch_text", "w") as f: f.write("write") files = os.listdir(d) assert "scratch_text" in files @@ -128,7 +128,7 @@ def test_symlink(self): copy_to_current_on_exit=False, create_symbolic_link=True, ) as d: - with open("scratch_text", "w", encoding="utf-8") as f: + with open("scratch_text", "w") as f: f.write("write") files = os.listdir(d) assert "scratch_text" in files