diff --git a/CHANGES.rst b/CHANGES.rst index 5293cd874..386a759ab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +3.1.0 (unreleased) +------------------ + +The ASDF Standard is at v1.6.0 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Cleanup ``asdf.util`` including deprecating: ``human_list`` + ``resolve_name`` ``minversion`` and ``iter_subclasses`` [#1688] + 3.0.1 (2023-10-30) ------------------ diff --git a/asdf/_block/external.py b/asdf/_block/external.py index 6e5f41620..734e9a773 100644 --- a/asdf/_block/external.py +++ b/asdf/_block/external.py @@ -40,7 +40,7 @@ def load(self, base_uri, uri, memmap=False, validate_checksums=False): ) as af: blk = af._blocks.blocks[0] if memmap and blk.header["compression"] == b"\0\0\0\0": - parsed_url = util.patched_urllib_parse.urlparse(resolved_uri) + parsed_url = util._patched_urllib_parse.urlparse(resolved_uri) if parsed_url.scheme == "file": # deal with leading slash for windows file:// filename = urllib.request.url2pathname(parsed_url.path) @@ -58,7 +58,7 @@ def clear(self): def relative_uri_for_index(uri, index): # get the os-native separated path for this uri - path = util.patched_urllib_parse.urlparse(uri).path + path = util._patched_urllib_parse.urlparse(uri).path dirname, filename = os.path.split(path) filename = os.path.splitext(filename)[0] + f"{index:04d}.asdf" return filename diff --git a/asdf/_block/io.py b/asdf/_block/io.py index e0afa7d23..ce93f9a11 100644 --- a/asdf/_block/io.py +++ b/asdf/_block/io.py @@ -15,7 +15,7 @@ from .exceptions import BlockIndexError -BLOCK_HEADER = util.BinaryStruct( +BLOCK_HEADER = util._BinaryStruct( [ ("flags", "I"), ("compression", "4s"), @@ -83,7 +83,7 @@ def read_block_header(fd, offset=None): ------- header : dict Dictionary containing the read ASDF header as parsed by the - `BLOCK_HEADER` `asdf.util.BinaryStruct`. + `BLOCK_HEADER` `asdf.util._BinaryStruct`. Raises ------ diff --git a/asdf/_tests/test_deprecated.py b/asdf/_tests/test_deprecated.py index fa5ee6016..62ecd050b 100644 --- a/asdf/_tests/test_deprecated.py +++ b/asdf/_tests/test_deprecated.py @@ -2,6 +2,7 @@ import pytest +import asdf from asdf.exceptions import AsdfDeprecationWarning @@ -15,3 +16,23 @@ def test_asdf_stream_deprecation(): def test_asdf_asdf_SerializationContext_import_deprecation(): with pytest.warns(AsdfDeprecationWarning, match="importing SerializationContext from asdf.asdf"): from asdf.asdf import SerializationContext # noqa: F401 + + +def test_asdf_util_human_list_deprecation(): + with pytest.warns(AsdfDeprecationWarning, match="asdf.util.human_list is deprecated"): + asdf.util.human_list("a") + + +def test_asdf_util_resolve_name_deprecation(): + with pytest.warns(AsdfDeprecationWarning, match="asdf.util.resolve_name is deprecated"): + asdf.util.resolve_name("asdf.AsdfFile") + + +def test_asdf_util_minversion_deprecation(): + with pytest.warns(AsdfDeprecationWarning, match="asdf.util.minversion is deprecated"): + asdf.util.minversion("yaml", "3.1") + + +def test_asdf_util_iter_subclasses_deprecation(): + with pytest.warns(AsdfDeprecationWarning, match="asdf.util.iter_subclasses is deprecated"): + list(asdf.util.iter_subclasses(asdf.AsdfFile)) diff --git a/asdf/_tests/test_util.py b/asdf/_tests/test_util.py index e305bb25c..a0830b61d 100644 --- a/asdf/_tests/test_util.py +++ b/asdf/_tests/test_util.py @@ -1,8 +1,10 @@ import io +import warnings import pytest from asdf import generic_io, util +from asdf.exceptions import AsdfDeprecationWarning def test_is_primitive(): @@ -35,12 +37,12 @@ def test_get_class_name(): def test_patched_urllib_parse(): - assert "asdf" in util.patched_urllib_parse.uses_relative - assert "asdf" in util.patched_urllib_parse.uses_netloc + assert "asdf" in util._patched_urllib_parse.uses_relative + assert "asdf" in util._patched_urllib_parse.uses_netloc import urllib.parse - assert urllib.parse is not util.patched_urllib_parse + assert urllib.parse is not util._patched_urllib_parse assert "asdf" not in urllib.parse.uses_relative assert "asdf" not in urllib.parse.uses_netloc @@ -103,12 +105,14 @@ def test_minversion(): good_versions = ["1.16", "1.16.1", "1.16.0.dev", "1.16dev"] bad_versions = ["100000", "100000.2rc1"] - for version in good_versions: - assert util.minversion(np, version) - assert util.minversion("numpy", version) - for version in bad_versions: - assert not util.minversion(np, version) - assert not util.minversion("numpy", version) - - assert util.minversion(yaml, "3.1") - assert util.minversion("yaml", "3.1") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "asdf.util.minversion", AsdfDeprecationWarning) + for version in good_versions: + assert util.minversion(np, version) + assert util.minversion("numpy", version) + for version in bad_versions: + assert not util.minversion(np, version) + assert not util.minversion("numpy", version) + + assert util.minversion(yaml, "3.1") + assert util.minversion("yaml", "3.1") diff --git a/asdf/commands/diff.py b/asdf/commands/diff.py index 7c682fa74..711ad9c9c 100644 --- a/asdf/commands/diff.py +++ b/asdf/commands/diff.py @@ -33,7 +33,6 @@ import asdf from asdf.extension._serialization_context import BlockAccess from asdf.tagged import Tagged -from asdf.util import human_list from .main import Command @@ -256,6 +255,34 @@ def _load_array(asdf_file, array_dict): return conv.from_yaml_tree(array_dict, array_dict._tag, sctx) +def _human_list(line, separator="and"): + """ + Formats a list for human readability. + + Parameters + ---------- + line : sequence + A sequence of strings + + separator : string, optional + The word to use between the last two entries. Default: + ``"and"``. + + Returns + ------- + formatted_list : string + + Examples + -------- + >>> _human_list(["vanilla", "strawberry", "chocolate"], "or") + 'vanilla, strawberry or chocolate' + """ + if len(line) == 1: + return line[0] + + return ", ".join(line[:-1]) + " " + separator + " " + line[-1] + + def compare_ndarrays(diff_ctx, array0, array1, keys): """Compares two ndarray objects""" if isinstance(array0, list): @@ -278,7 +305,7 @@ def compare_ndarrays(diff_ctx, array0, array1, keys): differences.append("contents") if differences: - msg = f"ndarrays differ by {human_list(differences)}" + msg = f"ndarrays differ by {_human_list(differences)}" print_in_tree(diff_ctx, keys, msg, False, ignore_lwl=True) print_in_tree(diff_ctx, keys, msg, True, ignore_lwl=True) diff --git a/asdf/commands/main.py b/asdf/commands/main.py index 057181eb4..d927f55e8 100644 --- a/asdf/commands/main.py +++ b/asdf/commands/main.py @@ -37,7 +37,7 @@ def help_(args): help_parser = subparsers.add_parser("help", help="Display usage information") help_parser.set_defaults(func=help_) - commands = {x.__name__: x for x in util.iter_subclasses(Command)} + commands = {x.__name__: x for x in util._iter_subclasses(Command)} for command in command_order: commands[str(command)].setup_arguments(subparsers) diff --git a/asdf/core/_converters/complex.py b/asdf/core/_converters/complex.py index acf95c98d..52e2f6daa 100644 --- a/asdf/core/_converters/complex.py +++ b/asdf/core/_converters/complex.py @@ -14,7 +14,7 @@ class ComplexConverter(Converter): tags = ["tag:stsci.edu:asdf/core/complex-1.0.0"] - types = [*list(util.iter_subclasses(np.complexfloating)), complex] + types = [*list(util._iter_subclasses(np.complexfloating)), complex] def to_yaml_tree(self, obj, tag, ctx): return str(obj) diff --git a/asdf/generic_io.py b/asdf/generic_io.py index f4cb42d9d..1ebe660fc 100644 --- a/asdf/generic_io.py +++ b/asdf/generic_io.py @@ -22,7 +22,7 @@ from . import util from .exceptions import DelimiterNotFoundError from .extern import atomicfile -from .util import patched_urllib_parse +from .util import _patched_urllib_parse __all__ = ["get_file", "get_uri", "resolve_uri", "relative_uri"] @@ -69,8 +69,8 @@ def resolve_uri(base, uri): """ if base is None: base = "" - resolved = patched_urllib_parse.urljoin(base, uri) - parsed = patched_urllib_parse.urlparse(resolved) + resolved = _patched_urllib_parse.urljoin(base, uri) + parsed = _patched_urllib_parse.urlparse(resolved) if parsed.path != "" and not parsed.path.startswith("/"): msg = "Resolved to relative URL" raise ValueError(msg) @@ -81,8 +81,8 @@ def relative_uri(source, target): """ Make a relative URI from source to target. """ - su = patched_urllib_parse.urlparse(source) - tu = patched_urllib_parse.urlparse(target) + su = _patched_urllib_parse.urlparse(source) + tu = _patched_urllib_parse.urlparse(target) extra = list(tu[3:]) relative = None if tu[0] == "" and tu[1] == "": @@ -98,7 +98,7 @@ def relative_uri(source, target): if relative == ".": relative = "" - return patched_urllib_parse.urlunparse(["", "", relative, *extra]) + return _patched_urllib_parse.urlunparse(["", "", relative, *extra]) class _TruncatedReader: @@ -187,7 +187,7 @@ def read(self, nbytes=None): return content -class GenericFile(metaclass=util.InheritDocstrings): +class GenericFile(metaclass=util._InheritDocstrings): """ Base class for an abstraction layer around a number of different file-like types. Each of its subclasses handles a particular kind @@ -1094,7 +1094,7 @@ def get_file(init, mode="r", uri=None, close=False): return GenericWrapper(init) if isinstance(init, (str, pathlib.Path)): - parsed = patched_urllib_parse.urlparse(str(init)) + parsed = _patched_urllib_parse.urlparse(str(init)) if parsed.scheme in ["http", "https"]: if "w" in mode: msg = "HTTP connections can not be opened for writing" diff --git a/asdf/reference.py b/asdf/reference.py index 91f835efd..6d4250c5c 100644 --- a/asdf/reference.py +++ b/asdf/reference.py @@ -12,7 +12,7 @@ import numpy as np from . import generic_io, treeutil, util -from .util import patched_urllib_parse +from .util import _patched_urllib_parse __all__ = ["resolve_fragment", "Reference", "find_references", "resolve_references", "make_reference"] @@ -22,7 +22,7 @@ def resolve_fragment(tree, pointer): Resolve a JSON Pointer within the tree. """ pointer = pointer.lstrip("/") - parts = patched_urllib_parse.unquote(pointer).split("/") if pointer else [] + parts = _patched_urllib_parse.unquote(pointer).split("/") if pointer else [] for part in parts: part_ = part.replace("~1", "/").replace("~0", "~") @@ -57,7 +57,7 @@ def _get_target(self, **kwargs): base_uri = self._asdffile().uri uri = generic_io.resolve_uri(base_uri, self._uri) asdffile = self._asdffile().open_external(uri, **kwargs) - parts = patched_urllib_parse.urlparse(self._uri) + parts = _patched_urllib_parse.urlparse(self._uri) fragment = parts.fragment self._target = resolve_fragment(asdffile.tree, fragment) return self._target diff --git a/asdf/schema.py b/asdf/schema.py index 477fc1000..f0daefaca 100644 --- a/asdf/schema.py +++ b/asdf/schema.py @@ -17,7 +17,7 @@ from . import constants, generic_io, reference, tagged, treeutil, util, versioning, yamlutil from .config import get_config from .exceptions import AsdfDeprecationWarning, AsdfWarning -from .util import patched_urllib_parse +from .util import _patched_urllib_parse YAML_SCHEMA_METASCHEMA_ID = "http://stsci.edu/schemas/yaml-schema/draft-01" @@ -381,7 +381,7 @@ def get_schema(url): # Supplying our own implementation of urljoin_cache # allows asdf:// URIs to be resolved correctly. - urljoin_cache = lru_cache(1024)(patched_urllib_parse.urljoin) + urljoin_cache = lru_cache(1024)(_patched_urllib_parse.urljoin) # We set cache_remote=False here because we do the caching of # remote schemas here in `load_schema`, so we don't need diff --git a/asdf/util.py b/asdf/util.py index 47b26ffef..36cafd3ed 100644 --- a/asdf/util.py +++ b/asdf/util.py @@ -5,6 +5,7 @@ import re import struct import types +import warnings from functools import lru_cache from importlib import metadata from urllib.request import pathname2url @@ -20,21 +21,22 @@ from packaging.version import Version from . import constants +from .exceptions import AsdfDeprecationWarning # We're importing our own copy of urllib.parse because # we need to patch it to support asdf:// URIs, but it'd # be irresponsible to do this for all users of a # standard library. urllib_parse_spec = importlib.util.find_spec("urllib.parse") -patched_urllib_parse = importlib.util.module_from_spec(urllib_parse_spec) -urllib_parse_spec.loader.exec_module(patched_urllib_parse) +_patched_urllib_parse = importlib.util.module_from_spec(urllib_parse_spec) +urllib_parse_spec.loader.exec_module(_patched_urllib_parse) del urllib_parse_spec # urllib.parse needs to know that it should treat asdf:// # URIs like http:// URIs for the purposes of joining # a relative path to a base URI. -patched_urllib_parse.uses_relative.append("asdf") -patched_urllib_parse.uses_netloc.append("asdf") +_patched_urllib_parse.uses_relative.append("asdf") +_patched_urllib_parse.uses_netloc.append("asdf") __all__ = [ @@ -49,6 +51,8 @@ "is_primitive", "uri_match", "get_class_name", + "get_file_type", + "FileType", ] @@ -71,9 +75,10 @@ def human_list(line, separator="and"): Examples -------- - >>> human_list(["vanilla", "strawberry", "chocolate"], "or") + >>> human_list(["vanilla", "strawberry", "chocolate"], "or") # doctest: +SKIP 'vanilla, strawberry or chocolate' """ + warnings.warn("asdf.util.human_list is deprecated", AsdfDeprecationWarning) if len(line) == 1: return line[0] @@ -97,24 +102,32 @@ def get_base_uri(uri): """ For a given URI, return the part without any fragment. """ - parts = patched_urllib_parse.urlparse(uri) - return patched_urllib_parse.urlunparse([*list(parts[:5]), ""]) + parts = _patched_urllib_parse.urlparse(uri) + return _patched_urllib_parse.urlunparse([*list(parts[:5]), ""]) def filepath_to_url(path): """ For a given local file path, return a file:// url. """ - return patched_urllib_parse.urljoin("file:", pathname2url(path)) + return _patched_urllib_parse.urljoin("file:", pathname2url(path)) -def iter_subclasses(cls): +def _iter_subclasses(cls): """ Returns all subclasses of a class. """ for x in cls.__subclasses__(): yield x - yield from iter_subclasses(x) + yield from _iter_subclasses(x) + + +def iter_subclasses(cls): + """ + Returns all subclasses of a class. + """ + warnings.warn("asdf.util.iter_subclasses is deprecated", AsdfDeprecationWarning) + yield from _iter_subclasses(cls) def calculate_padding(content_size, pad_blocks, block_size): @@ -151,7 +164,7 @@ def calculate_padding(content_size, pad_blocks, block_size): return max(new_size - content_size, 0) -class BinaryStruct: +class _BinaryStruct: """ A wrapper around the Python stdlib struct module to define a binary struct more like a dictionary than a tuple. @@ -267,7 +280,7 @@ def resolve_name(name): Examples -------- - >>> resolve_name('asdf.util.resolve_name') + >>> resolve_name('asdf.util.resolve_name') # doctest: +SKIP Raises @@ -276,6 +289,8 @@ def resolve_name(name): If the module or named object is not found. """ + warnings.warn("asdf.util.resolve_name is deprecated, see astropy.utils.resolve_name", AsdfDeprecationWarning) + # Note: On python 2 these must be str objects and not unicode parts = [str(part) for part in name.split(".")] @@ -351,6 +366,8 @@ def minversion(module, version, inclusive=True): as opposed to strictly greater than (default: `True`). """ + warnings.warn("asdf.util.minversion is deprecated, see astropy.utils.minversion", AsdfDeprecationWarning) + if isinstance(module, types.ModuleType): module_name = module.__name__ module_version = getattr(module, "__version__", None) @@ -358,7 +375,9 @@ def minversion(module, version, inclusive=True): module_name = module module_version = None try: - module = resolve_name(module_name) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "asdf.util.resolve_name", AsdfDeprecationWarning) + module = resolve_name(module_name) except ImportError: return False else: @@ -382,7 +401,7 @@ def minversion(module, version, inclusive=True): return Version(module_version) > Version(version) -class InheritDocstrings(type): +class _InheritDocstrings(type): """ This metaclass makes methods of a class automatically have their docstrings filled in from the methods they override in the base @@ -396,8 +415,8 @@ class InheritDocstrings(type): For example:: - >>> from asdf.util import InheritDocstrings - >>> class A(metaclass=InheritDocstrings): + >>> from asdf.util import _InheritDocstrings + >>> class A(metaclass=_InheritDocstrings): ... def wiggle(self): ... "Wiggle the thingamajig" ... pass @@ -496,12 +515,16 @@ def _compile_uri_match_pattern(pattern): def get_file_type(fd): """ Determine the file type of an open GenericFile instance. + Parameters ---------- - fd : GenericFile + + fd : ``asdf.generic_io.GenericFile`` + Returns ------- - FileType + + `asdf.util.FileType` """ if fd.peek(5) == constants.ASDF_MAGIC: return FileType.ASDF @@ -514,7 +537,7 @@ def get_file_type(fd): class FileType(enum.Enum): """ - Enum representing file types recognized by asdf. + Enum representing if a file is ASDF, FITS or UNKNOWN. """ ASDF = 1 diff --git a/asdf/yamlutil.py b/asdf/yamlutil.py index 67b681ef8..e4b1d5049 100644 --- a/asdf/yamlutil.py +++ b/asdf/yamlutil.py @@ -119,10 +119,10 @@ def represent_ordereddict(dumper, data): # Handle numpy scalars -for scalar_type in util.iter_subclasses(np.floating): +for scalar_type in util._iter_subclasses(np.floating): AsdfDumper.add_representer(scalar_type, lambda dumper, data: dumper.represent_float(float(data))) -for scalar_type in util.iter_subclasses(np.integer): +for scalar_type in util._iter_subclasses(np.integer): AsdfDumper.add_representer(scalar_type, lambda dumper, data: dumper.represent_int(int(data))) diff --git a/docs/asdf/deprecations.rst b/docs/asdf/deprecations.rst index 3b952870f..64f784cae 100644 --- a/docs/asdf/deprecations.rst +++ b/docs/asdf/deprecations.rst @@ -9,6 +9,16 @@ Deprecations Version 3.0 =========== +The following functions in ``asdf.util`` are deprecated: + +* ``human_list`` this is no longer part of the public API +* ``resolve_name`` see ``astropy.utils.resolve_name`` +* ``minversion`` see ``astropy.utils.minversion`` +* ``iter_subclasses`` this is no longer part of the public API + +Version 3.0 +=========== + SerializationContext was previously importable from ``asdf.asdf.SerializationContext``. Although not part of the public API, this import path has been deprecated and users should instead import ``SerializationContext`` from `asdf.extension`. diff --git a/pytest_asdf/plugin.py b/pytest_asdf/plugin.py index 469cf7fe9..f556cd5bd 100644 --- a/pytest_asdf/plugin.py +++ b/pytest_asdf/plugin.py @@ -66,9 +66,7 @@ def from_parent( **kwargs, ): # Fix for depreciation of fspath in pytest 7+ - from asdf.util import minversion - - if minversion("pytest", "7.0.0"): + if pytest.__version__ >= "7.0.0": path = pathlib.Path(fspath) kwargs["path"] = path else: