From 3cba0b912aa6b860f7264af33140302d0c5231cb Mon Sep 17 00:00:00 2001 From: Marcel Bargull Date: Sat, 27 Jan 2024 00:45:02 +0100 Subject: [PATCH] Dist removal (#5074) * Copy over Dist and related functions from conda * Copy over plan instructions from conda * Remove Dist * Inline one-off execute_/install_actions functions * Simplify _plan_from_actions * Collapse _plan_from_actions into execute_actions * Remove unused args for execute/install_actions * Use PackageRecord "prec" naming when unambiguous * Move install actions to .environ * Mark functions from conda.plan as non-public These will be changed/superseded/removed in a future version and thus should not be used elsewhere. --------- Signed-off-by: Marcel Bargull Co-authored-by: Ken Odegard Co-authored-by: Jannis Leidel --- CHANGELOG.md | 12 ++ conda_build/build.py | 2 +- conda_build/conda_interface.py | 31 +++-- conda_build/environ.py | 203 +++++++++++++++++++++++++++++++-- conda_build/index.py | 4 +- conda_build/render.py | 101 ++++++++-------- conda_build/utils.py | 13 ++- tests/test_api_build.py | 12 -- 8 files changed, 291 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1937ae4c4..5777412269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,15 @@ * Mark `conda_build.inspect_pkg.check_install(package)` as pending deprecation in favor of `conda_build.inspect_pkg.check_install(subdir)`. (#5033) * Mark `conda_build.inspect_pkg.check_install(prepend)` as pending deprecation. (#5033) * Mark `conda_build.inspect_pkg.check_install(minimal_hint)` as pending deprecation. (#5033) +* Mark `conda_build.conda_interface.Dist` as pending deprecation. (#5074) +* Mark `conda_build.conda_interface.display_actions` as pending deprecation. (#5074) +* Mark `conda_build.conda_interface.execute_actions` as pending deprecation. (#5074) +* Mark `conda_build.conda_interface.execute_plan` as pending deprecation. (#5074) +* Mark `conda_build.conda_interface.get_index` as pending deprecation. (#5074) +* Mark `conda_build.conda_interface.install_actions` as pending deprecation. (#5074) +* Mark `conda_build.conda_interface.linked` as pending deprecation. (#5074) +* Mark `conda_build.conda_interface.linked_data` as pending deprecation. (#5074) +* Mark `conda_build.utils.linked_data_no_multichannels` as pending deprecation. (#5074) * Remove `conda_build.api.update_index`. (#5151) * Remove `conda_build.cli.main_build.main`. (#5151) * Remove `conda_build.cli.main_convert.main`. (#5151) @@ -77,6 +86,9 @@ ### Other * Remove unused Allure test report collection. (#5113) +* Remove dependency on `conda.plan`. (#5074) +* Remove almost all dependency on `conda.models.dist`. (#5074) +* Replace usage of legacy `conda.models.dist.Dist` with `conda.models.records.PackageRecord`. (#5074) ### Contributors diff --git a/conda_build/build.py b/conda_build/build.py index fe0b5fe5a4..45f64995f2 100644 --- a/conda_build/build.py +++ b/conda_build/build.py @@ -3841,7 +3841,7 @@ def build_tree( meta, actions, "host", - package_subset=dep, + package_subset=[dep], require_files=True, ) # test that package, using the local channel so that our new diff --git a/conda_build/conda_interface.py b/conda_build/conda_interface.py index 929384e459..4fa9fb3777 100644 --- a/conda_build/conda_interface.py +++ b/conda_build/conda_interface.py @@ -33,6 +33,7 @@ InstalledPackages, MatchSpec, NoPackagesFound, + PackageRecord, PathType, Resolve, StringIO, @@ -43,23 +44,15 @@ _toposort, add_parser_channels, add_parser_prefix, - display_actions, download, - execute_actions, - execute_plan, - get_index, handle_proxy_407, hashsum_file, human_bytes, input, - install_actions, lchmod, - linked, - linked_data, md5_file, memoized, normalized_version, - package_cache, prefix_placeholder, rm_rf, spec_from_line, @@ -72,8 +65,28 @@ walk_prefix, win_path_to_unix, ) +from conda.exports import display_actions as _display_actions +from conda.exports import execute_actions as _execute_actions +from conda.exports import execute_plan as _execute_plan +from conda.exports import get_index as _get_index +from conda.exports import install_actions as _install_actions +from conda.exports import linked as _linked +from conda.exports import linked_data as _linked_data +from conda.exports import package_cache as _package_cache from conda.models.channel import get_conda_build_local_url # noqa: F401 -from conda.models.dist import Dist # noqa: F401 +from conda.models.dist import Dist as _Dist + +from .deprecations import deprecated + +deprecated.constant("24.1.0", "24.3.0", "Dist", _Dist) +deprecated.constant("24.1.0", "24.3.0", "display_actions", _display_actions) +deprecated.constant("24.1.0", "24.3.0", "execute_actions", _execute_actions) +deprecated.constant("24.1.0", "24.3.0", "execute_plan", _execute_plan) +deprecated.constant("24.1.0", "24.3.0", "get_index", _get_index) +deprecated.constant("24.1.0", "24.3.0", "install_actions", _install_actions) +deprecated.constant("24.1.0", "24.3.0", "linked", _linked) +deprecated.constant("24.1.0", "24.3.0", "linked_data", _linked_data) +deprecated.constant("24.1.0", "24.3.0", "package_cache", _package_cache) # TODO: Go to references of all properties below and import them from `context` instead binstar_upload = context.binstar_upload diff --git a/conda_build/environ.py b/conda_build/environ.py index 52f5e835ed..c363588e3f 100644 --- a/conda_build/environ.py +++ b/conda_build/environ.py @@ -10,24 +10,35 @@ import subprocess import sys import warnings +from collections import defaultdict from functools import lru_cache from glob import glob +from logging import getLogger from os.path import join, normpath +from conda.base.constants import DEFAULTS_CHANNEL_NAME, UNKNOWN_CHANNEL +from conda.common.io import env_vars +from conda.core.index import LAST_CHANNEL_URLS +from conda.core.link import PrefixSetup, UnlinkLinkTransaction +from conda.core.prefix_data import PrefixData +from conda.models.channel import prioritize_channels + from . import utils from .conda_interface import ( + Channel, CondaError, LinkError, LockError, + MatchSpec, NoPackagesFoundError, + PackageRecord, PaddingError, + ProgressiveFetchExtract, TemporaryDirectory, UnsatisfiableError, + context, create_default_packages, - display_actions, - execute_actions, get_version_from_git_tag, - install_actions, pkgs_dirs, reset_context, root_dir, @@ -42,10 +53,16 @@ env_var, on_mac, on_win, + package_record_to_requirement, prepend_bin_path, ) from .variants import get_default_variant +log = getLogger(__name__) + +PREFIX_ACTION = "PREFIX" +LINK_ACTION = "LINK" + # these are things that we provide env vars for more explicitly. This list disables the # pass-through of variant values to env vars for these keys. LANGUAGES = ("PERL", "LUA", "R", "NUMPY", "PYTHON") @@ -890,8 +907,8 @@ def get_install_actions( disable_pip, ) in cached_actions and last_index_ts >= index_ts: actions = cached_actions[(specs, env, subdir, channel_urls, disable_pip)].copy() - if "PREFIX" in actions: - actions["PREFIX"] = prefix + if PREFIX_ACTION in actions: + actions[PREFIX_ACTION] = prefix elif specs: # this is hiding output like: # Fetching package metadata ........... @@ -899,7 +916,7 @@ def get_install_actions( with utils.LoggingContext(conda_log_level): with capture(): try: - actions = install_actions(prefix, index, specs, force=True) + actions = _install_actions(prefix, index, specs) except (NoPackagesFoundError, UnsatisfiableError) as exc: raise DependencyNeedsBuildingError(exc, subdir=subdir) except ( @@ -973,8 +990,8 @@ def get_install_actions( if not any( re.match(r"^%s(?:$|[\s=].*)" % pkg, str(dep)) for dep in specs ): - actions["LINK"] = [ - spec for spec in actions["LINK"] if spec.name != pkg + actions[LINK_ACTION] = [ + prec for prec in actions[LINK_ACTION] if prec.name != pkg ] utils.trim_empty_keys(actions) cached_actions[(specs, env, subdir, channel_urls, disable_pip)] = actions.copy() @@ -1051,13 +1068,13 @@ def create_env( timeout=config.timeout, ) utils.trim_empty_keys(actions) - display_actions(actions, index) + _display_actions(actions) if utils.on_win: for k, v in os.environ.items(): os.environ[k] = str(v) with env_var("CONDA_QUIET", not config.verbose, reset_context): with env_var("CONDA_JSON", not config.verbose, reset_context): - execute_actions(actions, index) + _execute_actions(actions) except ( SystemExit, PaddingError, @@ -1096,7 +1113,7 @@ def create_env( # Set this here and use to create environ # Setting this here is important because we use it below (symlink) prefix = config.host_prefix if host else config.build_prefix - actions["PREFIX"] = prefix + actions[PREFIX_ACTION] = prefix create_env( prefix, @@ -1234,6 +1251,168 @@ def get_pinned_deps(m, section): channel_urls=tuple(m.config.channel_urls), ) runtime_deps = [ - " ".join(link.dist_name.rsplit("-", 2)) for link in actions.get("LINK", []) + package_record_to_requirement(prec) for prec in actions.get(LINK_ACTION, []) ] return runtime_deps + + +# NOTE: The function has to retain the "install_actions" name for now since +# conda_libmamba_solver.solver.LibMambaSolver._called_from_conda_build +# checks for this name in the call stack explicitly. +def install_actions(prefix, index, specs): + # This is copied over from https://github.com/conda/conda/blob/23.11.0/conda/plan.py#L471 + # but reduced to only the functionality actually used within conda-build. + + with env_vars( + { + "CONDA_ALLOW_NON_CHANNEL_URLS": "true", + "CONDA_SOLVER_IGNORE_TIMESTAMPS": "false", + }, + callback=reset_context, + ): + # a hack since in conda-build we don't track channel_priority_map + if LAST_CHANNEL_URLS: + channel_priority_map = prioritize_channels(LAST_CHANNEL_URLS) + # tuple(dict.fromkeys(...)) removes duplicates while preserving input order. + channels = tuple( + dict.fromkeys(Channel(url) for url in channel_priority_map) + ) + subdirs = ( + tuple( + dict.fromkeys( + subdir for subdir in (c.subdir for c in channels) if subdir + ) + ) + or context.subdirs + ) + else: + channels = subdirs = None + + specs = tuple(MatchSpec(spec) for spec in specs) + + PrefixData._cache_.clear() + + solver_backend = context.plugin_manager.get_cached_solver_backend() + solver = solver_backend(prefix, channels, subdirs, specs_to_add=specs) + if index: + # Solver can modify the index (e.g., Solver._prepare adds virtual + # package) => Copy index (just outer container, not deep copy) + # to conserve it. + solver._index = index.copy() + txn = solver.solve_for_transaction(prune=False, ignore_pinned=False) + prefix_setup = txn.prefix_setups[prefix] + actions = { + PREFIX_ACTION: prefix, + LINK_ACTION: [prec for prec in prefix_setup.link_precs], + } + return actions + + +_install_actions = install_actions +del install_actions + + +def _execute_actions(actions): + # This is copied over from https://github.com/conda/conda/blob/23.11.0/conda/plan.py#L575 + # but reduced to only the functionality actually used within conda-build. + + assert PREFIX_ACTION in actions and actions[PREFIX_ACTION] + prefix = actions[PREFIX_ACTION] + + if LINK_ACTION not in actions: + log.debug(f"action {LINK_ACTION} not in actions") + return + + link_precs = actions[LINK_ACTION] + if not link_precs: + log.debug(f"action {LINK_ACTION} has None value") + return + + # Always link menuinst first/last on windows in case a subsequent + # package tries to import it to create/remove a shortcut + link_precs = [p for p in link_precs if p.name == "menuinst"] + [ + p for p in link_precs if p.name != "menuinst" + ] + + progressive_fetch_extract = ProgressiveFetchExtract(link_precs) + progressive_fetch_extract.prepare() + + stp = PrefixSetup(prefix, (), link_precs, (), [], ()) + unlink_link_transaction = UnlinkLinkTransaction(stp) + + log.debug(" %s(%r)", "PROGRESSIVEFETCHEXTRACT", progressive_fetch_extract) + progressive_fetch_extract.execute() + log.debug(" %s(%r)", "UNLINKLINKTRANSACTION", unlink_link_transaction) + unlink_link_transaction.execute() + + +def _display_actions(actions): + # This is copied over from https://github.com/conda/conda/blob/23.11.0/conda/plan.py#L58 + # but reduced to only the functionality actually used within conda-build. + + prefix = actions.get(PREFIX_ACTION) + builder = ["", "## Package Plan ##\n"] + if prefix: + builder.append(" environment location: %s" % prefix) + builder.append("") + print("\n".join(builder)) + + show_channel_urls = context.show_channel_urls + + def channel_str(rec): + if rec.get("schannel"): + return rec["schannel"] + if rec.get("url"): + return Channel(rec["url"]).canonical_name + if rec.get("channel"): + return Channel(rec["channel"]).canonical_name + return UNKNOWN_CHANNEL + + def channel_filt(s): + if show_channel_urls is False: + return "" + if show_channel_urls is None and s == DEFAULTS_CHANNEL_NAME: + return "" + return s + + packages = defaultdict(lambda: "") + features = defaultdict(lambda: "") + channels = defaultdict(lambda: "") + + for prec in actions.get(LINK_ACTION, []): + assert isinstance(prec, PackageRecord) + pkg = prec["name"] + channels[pkg] = channel_filt(channel_str(prec)) + packages[pkg] = prec["version"] + "-" + prec["build"] + features[pkg] = ",".join(prec.get("features") or ()) + + fmt = {} + if packages: + maxpkg = max(len(p) for p in packages) + 1 + maxver = max(len(p) for p in packages.values()) + maxfeatures = max(len(p) for p in features.values()) + maxchannels = max(len(p) for p in channels.values()) + for pkg in packages: + # That's right. I'm using old-style string formatting to generate a + # string with new-style string formatting. + fmt[pkg] = f"{{pkg:<{maxpkg}}} {{vers:<{maxver}}}" + if maxchannels: + fmt[pkg] += " {channel:<%s}" % maxchannels + if features[pkg]: + fmt[pkg] += " [{features:<%s}]" % maxfeatures + + lead = " " * 4 + + def format(s, pkg): + return lead + s.format( + pkg=pkg + ":", + vers=packages[pkg], + channel=channels[pkg], + features=features[pkg], + ) + + if packages: + print("\nThe following NEW packages will be INSTALLED:\n") + for pkg in sorted(packages): + print(format(fmt[pkg], pkg)) + print() diff --git a/conda_build/index.py b/conda_build/index.py index 9a0004e771..aebc28fe21 100644 --- a/conda_build/index.py +++ b/conda_build/index.py @@ -37,6 +37,7 @@ from conda.common.compat import ensure_binary # BAD BAD BAD - conda internals +from conda.core.index import get_index from conda.core.subdir_data import SubdirData from conda.models.channel import Channel from conda_index.index import update_index as _update_index @@ -57,7 +58,6 @@ TemporaryDirectory, VersionOrder, context, - get_index, human_bytes, url_path, ) @@ -226,7 +226,7 @@ def get_build_index( platform=subdir, ) - expanded_channels = {rec.channel for rec in cached_index.values()} + expanded_channels = {rec.channel for rec in cached_index} superchannel = {} # we need channeldata.json too, as it is a more reliable source of run_exports data diff --git a/conda_build/render.py b/conda_build/render.py index c97f3bbe9f..c75838a65b 100644 --- a/conda_build/render.py +++ b/conda_build/render.py @@ -28,17 +28,22 @@ from . import environ, exceptions, source, utils from .conda_interface import ( + PackageRecord, ProgressiveFetchExtract, TemporaryDirectory, UnsatisfiableError, - execute_actions, pkgs_dirs, specs_from_url, ) +from .environ import LINK_ACTION from .exceptions import DependencyNeedsBuildingError from .index import get_build_index from .metadata import MetaData, combine_top_level_metadata_with_output -from .utils import CONDA_PACKAGE_EXTENSION_V1, CONDA_PACKAGE_EXTENSION_V2 +from .utils import ( + CONDA_PACKAGE_EXTENSION_V1, + CONDA_PACKAGE_EXTENSION_V2, + package_record_to_requirement, +) from .variants import ( filter_by_key_value, get_package_variants, @@ -86,11 +91,8 @@ def bldpkg_path(m): def actions_to_pins(actions): - if "LINK" in actions: - return [ - " ".join(spec.dist_name.split()[0].rsplit("-", 2)) - for spec in actions["LINK"] - ] + if LINK_ACTION in actions: + return [package_record_to_requirement(prec) for prec in actions[LINK_ACTION]] return [] @@ -340,37 +342,40 @@ def execute_download_actions(m, actions, env, package_subset=None, require_files # this should be just downloading packages. We don't need to extract them - - download_actions = { - k: v for k, v in actions.items() if k in ("FETCH", "EXTRACT", "PREFIX") - } - if "FETCH" in actions or "EXTRACT" in actions: - # this is to force the download - execute_actions(download_actions, index, verbose=m.config.debug) + # NOTE: The following commented execute_actions is defunct + # (FETCH/EXTRACT were replaced by PROGRESSIVEFETCHEXTRACT). + # + # download_actions = { + # k: v for k, v in actions.items() if k in (FETCH, EXTRACT, PREFIX) + # } + # if FETCH in actions or EXTRACT in actions: + # # this is to force the download + # execute_actions(download_actions, index, verbose=m.config.debug) pkg_files = {} - packages = actions.get("LINK", []) - package_subset = utils.ensure_list(package_subset) + precs = actions.get(LINK_ACTION, []) + if isinstance(package_subset, PackageRecord): + package_subset = [package_subset] + else: + package_subset = utils.ensure_list(package_subset) selected_packages = set() if package_subset: for pkg in package_subset: - if hasattr(pkg, "name"): - if pkg in packages: - selected_packages.add(pkg) + if isinstance(pkg, PackageRecord): + prec = pkg + if prec in precs: + selected_packages.add(prec) else: pkg_name = pkg.split()[0] - for link_pkg in packages: - if pkg_name == link_pkg.name: - selected_packages.add(link_pkg) + for link_prec in precs: + if pkg_name == link_prec.name: + selected_packages.add(link_prec) break - packages = selected_packages + precs = selected_packages - for pkg in packages: - if hasattr(pkg, "dist_name"): - pkg_dist = pkg.dist_name - else: - pkg = strip_channel(pkg) - pkg_dist = pkg.split(" ")[0] + for prec in precs: + pkg_dist = "-".join((prec.name, prec.version, prec.build)) pkg_loc = find_pkg_dir_or_file_in_pkgs_dirs( pkg_dist, m, files_only=require_files ) @@ -379,22 +384,21 @@ def execute_download_actions(m, actions, env, package_subset=None, require_files # TODO: this is a vile hack reaching into conda's internals. Replace with # proper conda API when available. if not pkg_loc: - try: - pkg_record = [_ for _ in index if _.dist_name == pkg_dist][0] - # the conda 4.4 API uses a single `link_prefs` kwarg - # whereas conda 4.3 used `index` and `link_dists` kwargs - pfe = ProgressiveFetchExtract(link_prefs=(index[pkg_record],)) - except TypeError: - # TypeError: __init__() got an unexpected keyword argument 'link_prefs' - pfe = ProgressiveFetchExtract(link_dists=[pkg], index=index) + link_prec = [ + rec + for rec in index + if (rec.name, rec.version, rec.build) + == (prec.name, prec.version, prec.build) + ][0] + pfe = ProgressiveFetchExtract(link_prefs=(link_prec,)) with utils.LoggingContext(): pfe.execute() for pkg_dir in pkgs_dirs: - _loc = join(pkg_dir, index.get(pkg, pkg).fn) + _loc = join(pkg_dir, prec.fn) if isfile(_loc): pkg_loc = _loc break - pkg_files[pkg] = pkg_loc, pkg_dist + pkg_files[prec] = pkg_loc, pkg_dist return pkg_files @@ -404,27 +408,30 @@ def get_upstream_pins(m: MetaData, actions, env): downstream dependency specs. Return these additional specs.""" env_specs = m.get_value(f"requirements/{env}", []) explicit_specs = [req.split(" ")[0] for req in env_specs] if env_specs else [] - linked_packages = actions.get("LINK", []) - linked_packages = [pkg for pkg in linked_packages if pkg.name in explicit_specs] + linked_packages = actions.get(LINK_ACTION, []) + linked_packages = [prec for prec in linked_packages if prec.name in explicit_specs] ignore_pkgs_list = utils.ensure_list(m.get_value("build/ignore_run_exports_from")) ignore_list = utils.ensure_list(m.get_value("build/ignore_run_exports")) additional_specs = {} - for pkg in linked_packages: - if any(pkg.name in req.split(" ")[0] for req in ignore_pkgs_list): + for prec in linked_packages: + if any(prec.name in req.split(" ")[0] for req in ignore_pkgs_list): continue run_exports = None if m.config.use_channeldata: - channeldata = utils.download_channeldata(pkg.channel) + channeldata = utils.download_channeldata(prec.channel) # only use channeldata if requested, channeldata exists and contains # a packages key, otherwise use run_exports from the packages themselves if "packages" in channeldata: - pkg_data = channeldata["packages"].get(pkg.name, {}) - run_exports = pkg_data.get("run_exports", {}).get(pkg.version, {}) + pkg_data = channeldata["packages"].get(prec.name, {}) + run_exports = pkg_data.get("run_exports", {}).get(prec.version, {}) if run_exports is None: loc, dist = execute_download_actions( - m, actions, env=env, package_subset=pkg - )[pkg] + m, + actions, + env=env, + package_subset=[prec], + )[prec] run_exports = _read_specs_from_package(loc, dist) specs = _filter_run_exports(run_exports, ignore_list) if specs: diff --git a/conda_build/utils.py b/conda_build/utils.py index 1c303a8b64..bc1c20634c 100644 --- a/conda_build/utils.py +++ b/conda_build/utils.py @@ -60,6 +60,7 @@ from .conda_interface import ( CondaHTTPError, MatchSpec, + PackageRecord, StringIO, TemporaryDirectory, VersionOrder, @@ -75,6 +76,7 @@ win_path_to_unix, ) from .conda_interface import rm_rf as _rm_rf +from .deprecations import deprecated from .exceptions import BuildLockError on_win = sys.platform == "win32" @@ -1956,13 +1958,11 @@ def match_peer_job(target_matchspec, other_m, this_m=None): for any keys that are shared between target_variant and m.config.variant""" name, version, build = other_m.name(), other_m.version(), "" matchspec_matches = target_matchspec.match( - Dist( + PackageRecord( name=name, - dist_name=f"{name}-{version}-{build}", version=version, - build_string=build, + build=build, build_number=other_m.build_number(), - channel=None, ) ) @@ -2110,6 +2110,7 @@ def download_channeldata(channel_url): return data +@deprecated("24.1.0", "24.3.0") def linked_data_no_multichannels( prefix: str | os.PathLike | Path, ) -> dict[Dist, PrefixRecord]: @@ -2165,3 +2166,7 @@ def is_conda_pkg(pkg_path: str) -> bool: return path.is_file() and ( any(path.name.endswith(ext) for ext in CONDA_PACKAGE_EXTENSIONS) ) + + +def package_record_to_requirement(prec: PackageRecord) -> str: + return f"{prec.name} {prec.version} {prec.build}" diff --git a/tests/test_api_build.py b/tests/test_api_build.py index c9bc2cd31e..2e0f2b0224 100644 --- a/tests/test_api_build.py +++ b/tests/test_api_build.py @@ -33,10 +33,8 @@ from conda_build import __version__, api, exceptions from conda_build.conda_interface import ( - CONDA_VERSION, CondaError, LinkError, - VersionOrder, context, reset_context, url_path, @@ -1949,16 +1947,6 @@ def test_add_pip_as_python_dependency_from_condarc_file( Test whether settings from .condarc files are heeded. ref: https://github.com/conda/conda-libmamba-solver/issues/393 """ - if VersionOrder(CONDA_VERSION) <= VersionOrder("23.10.0"): - if not add_pip_as_python_dependency and context.solver == "libmamba": - pytest.xfail( - "conda.plan.install_actions from conda<=23.10.0 ignores .condarc files." - ) - from conda.base.context import context_stack - - # ContextStack's pop/replace methods don't call self.apply. - context_stack.apply() - # TODO: SubdirData._cache_ clearing might not be needed for future conda versions. # See https://github.com/conda/conda/pull/13365 for proposed changes. from conda.core.subdir_data import SubdirData