Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Replace sys.exit calls in conda_build/metadata.py #5371

Merged
merged 15 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 58 additions & 96 deletions conda_build/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,27 @@
import warnings
from collections import OrderedDict
from functools import lru_cache
from os.path import isfile, join
from os.path import isdir, isfile, join
from typing import TYPE_CHECKING, NamedTuple, overload

import jinja2
import yaml
from bs4 import UnicodeDammit
from conda.base.context import context
from conda.base.context import locate_prefix_by_name
from conda.gateways.disk.read import compute_sum
from conda.models.match_spec import MatchSpec
from frozendict import deepfreeze

from . import exceptions, utils
from . import utils
from .config import Config, get_or_merge_config
from .deprecations import deprecated
from .exceptions import (
CondaBuildException,
CondaBuildUserError,
DependencyNeedsBuildingError,
RecipeError,
UnableToParse,
)
from .features import feature_list
from .license_family import ensure_valid_license_family
from .utils import (
Expand All @@ -46,18 +55,12 @@
)

if TYPE_CHECKING:
from pathlib import Path
from typing import Any, Literal, Self

OutputDict = dict[str, Any]
OutputTuple = tuple[OutputDict, "MetaData"]

try:
import yaml
except ImportError:
sys.exit(
"Error: could not import yaml (required to read meta.yaml "
"files of conda recipes)"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was that basically an implicit dependency? Do we require it now in meta.yml?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that it doesn't say "ruamel.yaml" but that package is certainly a regular dependency now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was previously an optional dependency but is currently listed as a requirement in meta.yml.

This particular code change is more "cleanup" vs "remove sys.exit" (per work originally done in this PR), so I will revert this change; once the broken-out parts of the "remove sys.exit calls" work are merged, the original PR will get rebased and will mostly focus on cleanup.


try:
Loader = yaml.CLoader
Expand Down Expand Up @@ -336,12 +339,12 @@ def select_lines(text: str, namespace: dict[str, Any], variants_in_place: bool)
if value:
lines.append(line)
except Exception as e:
sys.exit(
f"Error: Invalid selector in meta.yaml line {i + 1}:\n"
f"offending line:\n"
f"{line}\n"
raise CondaBuildUserError(
f"Invalid selector in meta.yaml line {i + 1}:\n"
f"offending selector:\n"
f" [{selector}]\n"
f"exception:\n"
f"{e.__class__.__name__}: {e}\n"
f" {e.__class__.__name__}: {e}\n"
)
return "\n".join(lines) + "\n"

Expand All @@ -350,16 +353,9 @@ def yamlize(data):
try:
return yaml.load(data, Loader=StringifyNumbersLoader)
except yaml.error.YAMLError as e:
if "{{" in data:
try:
import jinja2

jinja2 # Avoid pyflakes failure: 'jinja2' imported but unused
except ImportError:
raise exceptions.UnableToParseMissingJinja2(original=e)
beeankha marked this conversation as resolved.
Show resolved Hide resolved
print("Problematic recipe:", file=sys.stderr)
print(data, file=sys.stderr)
raise exceptions.UnableToParse(original=e)
raise UnableToParse(original=e)


def ensure_valid_fields(meta):
Expand Down Expand Up @@ -400,9 +396,7 @@ def _trim_None_strings(meta_dict):
def ensure_valid_noarch_value(meta):
build_noarch = meta.get("build", {}).get("noarch")
if build_noarch and build_noarch not in NOARCH_TYPES:
raise exceptions.CondaBuildException(
f"Invalid value for noarch: {build_noarch}"
)
raise CondaBuildException(f"Invalid value for noarch: {build_noarch}")


def _get_all_dependencies(metadata, envs=("host", "build", "run")):
Expand Down Expand Up @@ -444,7 +438,7 @@ def check_circular_dependencies(
error = "Circular dependencies in recipe: \n"
for pair in pairs:
error += " {} <-> {}\n".format(*pair)
raise exceptions.RecipeError(error)
raise RecipeError(error)


def _check_circular_dependencies(
Expand Down Expand Up @@ -477,7 +471,7 @@ def _check_circular_dependencies(
error = "Circular dependencies in recipe: \n"
for pair in pairs:
error += " {} <-> {}\n".format(*pair)
raise exceptions.RecipeError(error)
raise RecipeError(error)


def _check_run_constrained(metadata_tuples):
Expand All @@ -495,7 +489,7 @@ def _check_run_constrained(metadata_tuples):
f"Reason: {exc}"
)
if errors:
raise exceptions.RecipeError("\n".join(["", *errors]))
raise RecipeError("\n".join(["", *errors]))


def _variants_equal(metadata, output_metadata):
Expand Down Expand Up @@ -539,7 +533,7 @@ def ensure_matching_hashes(output_metadata):
error += "Mismatching package: {} (id {}); dep: {}; consumer package: {}\n".format(
*prob
)
raise exceptions.RecipeError(
raise RecipeError(
"Mismatching hashes in recipe. Exact pins in dependencies "
"that contribute to the hash often cause this. Can you "
"change one or more exact pins to version bound constraints?\n"
Expand Down Expand Up @@ -767,28 +761,18 @@ def _git_clean(source_meta):
and complain.
"""

git_rev_tags_old = ("git_branch", "git_tag")
git_rev = "git_rev"

git_rev_tags = (git_rev,) + git_rev_tags_old

has_rev_tags = tuple(bool(source_meta.get(tag, "")) for tag in git_rev_tags)
if sum(has_rev_tags) > 1:
msg = "Error: multiple git_revs:"
msg += ", ".join(
f"{key}" for key, has in zip(git_rev_tags, has_rev_tags) if has
)
sys.exit(msg)
keys = [key for key in (git_rev, "git_branch", "git_tag") if key in source_meta]
if not keys:
# git_branch, git_tag, nor git_rev specified, return as-is
return source_meta
elif len(keys) > 1:
raise CondaBuildUserError(f"Multiple git_revs: {', '.join(keys)}")

# make a copy of the input so we have no side-effects
ret_meta = source_meta.copy()
# loop over the old versions
for key, has in zip(git_rev_tags[1:], has_rev_tags[1:]):
# update if needed
if has:
ret_meta[git_rev_tags[0]] = ret_meta[key]
# and remove
ret_meta.pop(key, None)
beeankha marked this conversation as resolved.
Show resolved Hide resolved
ret_meta[git_rev] = ret_meta.pop(keys[0])

return ret_meta

Expand All @@ -801,15 +785,16 @@ def _str_version(package_meta):
return package_meta


def check_bad_chrs(s, field):
bad_chrs = "=@#$%^&*:;\"'\\|<>?/ "
def check_bad_chrs(value: str, field: str) -> None:
bad_chrs = set("=@#$%^&*:;\"'\\|<>?/ ")
if field in ("package/version", "build/string"):
bad_chrs += "-"
bad_chrs.add("-")
if field != "package/version":
bad_chrs += "!"
for c in bad_chrs:
if c in s:
sys.exit(f"Error: bad character '{c}' in {field}: {s}")
bad_chrs.add("!")
if invalid := bad_chrs.intersection(value):
raise CondaBuildUserError(
f"Bad character(s) ({''.join(sorted(invalid))}) in {field}: {value}."
)


def get_package_version_pin(build_reqs, name):
Expand Down Expand Up @@ -882,20 +867,17 @@ def build_string_from_metadata(metadata):
return build_str


# This really belongs in conda, and it is int conda.cli.common,
# but we don't presently have an API there.
def _get_env_path(env_name_or_path):
if not os.path.isdir(env_name_or_path):
for envs_dir in list(context.envs_dirs) + [os.getcwd()]:
path = os.path.join(envs_dir, env_name_or_path)
if os.path.isdir(path):
env_name_or_path = path
break
bootstrap_metadir = os.path.join(env_name_or_path, "conda-meta")
if not os.path.isdir(bootstrap_metadir):
print(f"Bootstrap environment '{env_name_or_path}' not found")
sys.exit(1)
return env_name_or_path
@deprecated(
"24.7", "24.9", addendum="Use `conda.base.context.locate_prefix_by_name` instead."
)
def _get_env_path(
env_name_or_path: str | os.PathLike | Path,
) -> str | os.PathLike | Path:
return (
env_name_or_path
if isdir(env_name_or_path)
else locate_prefix_by_name(env_name_or_path)
)


def _get_dependencies_from_environment(env_name_or_path):
Expand Down Expand Up @@ -1124,7 +1106,7 @@ def finalize_outputs_pass(
fm.name(),
deepfreeze({k: fm.config.variant[k] for k in fm.get_used_vars()}),
] = (output_d, fm)
except exceptions.DependencyNeedsBuildingError as e:
except DependencyNeedsBuildingError as e:
if not permit_unsatisfiable_variants:
raise
else:
Expand Down Expand Up @@ -1427,19 +1409,20 @@ def parse_until_resolved(
):
"""variant contains key-value mapping for additional functions and values
for jinja2 variables"""
# undefined_jinja_vars is refreshed by self.parse again
undefined_jinja_vars = ()
# store the "final" state that we think we're in. reloading the meta.yaml file
# can reset it (to True)
final = self.final
# always parse again at least once.

# always parse again at least once
self.parse_again(
permit_undefined_jinja=True,
allow_no_other_outputs=allow_no_other_outputs,
bypass_env_check=bypass_env_check,
)
self.final = final

# recursively parse again so long as each iteration has fewer undefined jinja variables
undefined_jinja_vars = ()
while set(undefined_jinja_vars) != set(self.undefined_jinja_vars):
undefined_jinja_vars = self.undefined_jinja_vars
self.parse_again(
Expand All @@ -1448,18 +1431,8 @@ def parse_until_resolved(
bypass_env_check=bypass_env_check,
)
self.final = final
if undefined_jinja_vars:
self.parse_again(
permit_undefined_jinja=False,
allow_no_other_outputs=allow_no_other_outputs,
bypass_env_check=bypass_env_check,
)
sys.exit(
f"Undefined Jinja2 variables remain ({self.undefined_jinja_vars}). Please enable "
"source downloading and try again."
)

# always parse again at the end, too.
# always parse again at the end without permit_undefined_jinja
self.parse_again(
permit_undefined_jinja=False,
allow_no_other_outputs=allow_no_other_outputs,
Expand Down Expand Up @@ -2024,17 +1997,6 @@ def _get_contents(
permit_undefined_jinja: If True, *any* use of undefined jinja variables will
evaluate to an emtpy string, without emitting an error.
"""
try:
import jinja2
except ImportError:
print("There was an error importing jinja2.", file=sys.stderr)
print(
"Please run `conda install jinja2` to enable jinja template support",
file=sys.stderr,
) # noqa
with open(self.meta_path) as fd:
return fd.read()

beeankha marked this conversation as resolved.
Show resolved Hide resolved
from .jinja_context import (
FilteredLoader,
UndefinedNeverFail,
Expand Down Expand Up @@ -2116,8 +2078,8 @@ def _get_contents(
except jinja2.TemplateError as ex:
if "'None' has not attribute" in str(ex):
ex = "Failed to run jinja context function"
sys.exit(
f"Error: Failed to render jinja template in {self.meta_path}:\n{str(ex)}"
raise CondaBuildUserError(
f"Failed to render jinja template in {self.meta_path}:\n{str(ex)}"
)
finally:
if "CONDA_BUILD_STATE" in os.environ:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_api_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ def test_recursive_fail(testing_config):

@pytest.mark.sanity
def test_jinja_typo(testing_config):
with pytest.raises(SystemExit, match="GIT_DSECRIBE_TAG"):
with pytest.raises(CondaBuildUserError, match="GIT_DSECRIBE_TAG"):
api.build(
os.path.join(fail_dir, "source_git_jinja2_oops"), config=testing_config
)
Expand Down
Loading
Loading