Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
42 changes: 37 additions & 5 deletions src/bentoml/_internal/bento/bento.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,23 @@ def append_model(model: BentoModelInfo) -> None:

@classmethod
def from_path(cls, path: PathType) -> Bento:
path = Path(path)
# Try to infer the tag from the directory structure (store_base/name/version/)
# This provides a fallback for old bento.yaml files that lack name/version fields.
inferred_tag: Tag | None = None
try:
version = path.name
name = path.parent.name
if name and version:
inferred_tag = Tag(name, version)
except (ValueError, TypeError):
pass

try:
with open(
os.path.join(path, BENTO_YAML_FILENAME), "r", encoding="utf-8"
) as bento_yaml:
info = BaseBentoInfo.from_yaml_file(bento_yaml)
info = BaseBentoInfo.from_yaml_file(bento_yaml, tag=inferred_tag)
except FileNotFoundError:
raise BentoMLException(
f"Failed to load bento because it does not contain a '{BENTO_YAML_FILENAME}'"
Expand Down Expand Up @@ -797,7 +809,9 @@ def dump(self, stream: t.IO[t.Any]) -> None:
return yaml.safe_dump(self.to_dict(), stream, sort_keys=False)

@classmethod
def from_yaml_file(cls, stream: t.IO[t.Any]) -> BaseBentoInfo:
def from_yaml_file(
cls, stream: t.IO[t.Any], tag: Tag | None = None
) -> BaseBentoInfo:
try:
yaml_content = yaml.safe_load(stream)
except yaml.YAMLError as exc:
Expand All @@ -806,9 +820,27 @@ def from_yaml_file(cls, stream: t.IO[t.Any]) -> BaseBentoInfo:

assert yaml_content is not None

yaml_content["tag"] = Tag(yaml_content["name"], yaml_content["version"])
del yaml_content["name"]
del yaml_content["version"]
try:
yaml_content["tag"] = Tag(yaml_content["name"], yaml_content["version"])
del yaml_content["name"]
del yaml_content["version"]
except KeyError:
if tag is not None:
yaml_content["tag"] = tag
yaml_content.pop("name", None)
yaml_content.pop("version", None)
else:
missing = []
if "name" not in yaml_content:
missing.append("name")
if "version" not in yaml_content:
missing.append("version")
raise BentoMLException(
f"Missing required field(s) {', '.join(missing)} in {BENTO_YAML_FILENAME}. "
f"This may be caused by an incompatible bento created with an older version of BentoML. "
f"Consider re-building the bento with the current version of BentoML or "
f"removing the outdated bento from the store."
) from None

# For backwards compatibility for bentos created prior to version 1.0.0rc1
if "runners" in yaml_content:
Expand Down
2 changes: 1 addition & 1 deletion src/bentoml/_internal/container/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def construct_containerfile(

with tempfile.TemporaryDirectory() as tempdir:
with open(bento.path_of("bento.yaml"), "rb") as bento_yaml:
options = BaseBentoInfo.from_yaml_file(bento_yaml)
options = BaseBentoInfo.from_yaml_file(bento_yaml, tag=bento.tag)
# tmpdir is our new build context.
shutil.copytree(bento.path, tempdir, dirs_exist_ok=True)

Expand Down
80 changes: 80 additions & 0 deletions tests/unit/_internal/bento/test_bento.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# pylint: disable=unused-argument
from __future__ import annotations

import io
import os
import posixpath
from datetime import datetime
Expand Down Expand Up @@ -374,3 +375,82 @@ def test_build_bento_with_args():
)
BentoMLContainer.bento_arguments.reset()
assert bento.info.args == {"label": "awesome"}


def test_from_yaml_file_missing_name_and_version_raises():
"""from_yaml_file should raise BentoMLException when name/version are missing
and no fallback tag is provided."""
from bentoml.exceptions import BentoMLException

yaml_content = """\
service: "service:svc"
bentoml_version: "1.1.1"
creation_time: '2024-07-26T17:03:11.523664+00:00'
labels: {}
models: []
runners: []
apis: []
docker:
distro: debian
python_version: '3.11'
python:
requirements_txt: null
conda:
environment_yml: null
"""
stream = io.StringIO(yaml_content)
with pytest.raises(BentoMLException, match="Missing required field"):
BaseBentoInfo.from_yaml_file(stream)


def test_from_yaml_file_missing_name_and_version_with_fallback_tag():
"""from_yaml_file should use the fallback tag when name/version are missing."""
yaml_content = """\
service: "service:svc"
bentoml_version: "1.1.1"
creation_time: '2024-07-26T17:03:11.523664+00:00'
labels: {}
models: []
runners: []
apis: []
docker:
distro: debian
python_version: '3.11'
python:
requirements_txt: null
conda:
environment_yml: null
"""
fallback_tag = Tag("myservice", "abc123")
stream = io.StringIO(yaml_content)
info = BaseBentoInfo.from_yaml_file(stream, tag=fallback_tag)
assert info.tag == fallback_tag
assert info.name == "myservice"
assert info.version == "abc123"


def test_from_yaml_file_missing_only_version_raises():
"""from_yaml_file should raise BentoMLException when version is missing
and no fallback tag is provided."""
from bentoml.exceptions import BentoMLException

yaml_content = """\
service: "service:svc"
name: myservice
bentoml_version: "1.1.1"
creation_time: '2024-07-26T17:03:11.523664+00:00'
labels: {}
models: []
runners: []
apis: []
docker:
distro: debian
python_version: '3.11'
python:
requirements_txt: null
conda:
environment_yml: null
"""
stream = io.StringIO(yaml_content)
with pytest.raises(BentoMLException, match="Missing required field.*version"):
BaseBentoInfo.from_yaml_file(stream)