mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
lockfile: drop 1.0 lockfile support (#9476)
This commit is contained in:
+4
-43
@@ -14,7 +14,6 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
|
|
||||||
from dvc.stage import serialize
|
from dvc.stage import serialize
|
||||||
from dvc.stage.exceptions import (
|
from dvc.stage.exceptions import (
|
||||||
StageFileBadNameError,
|
StageFileBadNameError,
|
||||||
@@ -358,36 +357,8 @@ class ProjectFile(FileMixin):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
def get_lockfile_schema(d):
|
|
||||||
from dvc.schema import COMPILED_LOCKFILE_V1_SCHEMA, COMPILED_LOCKFILE_V2_SCHEMA
|
|
||||||
|
|
||||||
schema = {
|
|
||||||
LOCKFILE_VERSION.V1: COMPILED_LOCKFILE_V1_SCHEMA,
|
|
||||||
LOCKFILE_VERSION.V2: COMPILED_LOCKFILE_V2_SCHEMA,
|
|
||||||
}
|
|
||||||
|
|
||||||
version = LOCKFILE_VERSION.from_dict(d)
|
|
||||||
return schema[version]
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_lock_v1_to_v2(d, version_info):
|
|
||||||
stages = dict(d)
|
|
||||||
|
|
||||||
for key in stages:
|
|
||||||
d.pop(key)
|
|
||||||
|
|
||||||
# forcing order, meta should always be at the top
|
|
||||||
d.update(version_info)
|
|
||||||
d["stages"] = stages
|
|
||||||
|
|
||||||
|
|
||||||
def lockfile_schema(data: _T) -> _T:
|
|
||||||
schema = get_lockfile_schema(data)
|
|
||||||
return schema(data)
|
|
||||||
|
|
||||||
|
|
||||||
class Lockfile(FileMixin):
|
class Lockfile(FileMixin):
|
||||||
SCHEMA = staticmethod(lockfile_schema) # type: ignore[assignment]
|
from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
|
||||||
|
|
||||||
def _verify_filename(self):
|
def _verify_filename(self):
|
||||||
pass # lockfile path is hardcoded, so no need to verify here
|
pass # lockfile path is hardcoded, so no need to verify here
|
||||||
@@ -401,21 +372,12 @@ class Lockfile(FileMixin):
|
|||||||
self._check_gitignored()
|
self._check_gitignored()
|
||||||
return {}, ""
|
return {}, ""
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_version_info(self):
|
|
||||||
version = LOCKFILE_VERSION.V2.value # pylint:disable=no-member
|
|
||||||
return {SCHEMA_KWD: version}
|
|
||||||
|
|
||||||
def dump(self, stage, **kwargs):
|
def dump(self, stage, **kwargs):
|
||||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||||
|
|
||||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||||
version = LOCKFILE_VERSION.from_dict(data)
|
if not data:
|
||||||
if version == LOCKFILE_VERSION.V1:
|
data.update({"schema": "2.0"})
|
||||||
logger.info("Migrating lock file '%s' from v1 to v2", self.relpath)
|
|
||||||
migrate_lock_v1_to_v2(data, self.latest_version_info)
|
|
||||||
elif not data:
|
|
||||||
data.update(self.latest_version_info)
|
|
||||||
# order is important, meta should always be at the top
|
# order is important, meta should always be at the top
|
||||||
logger.info("Generating lock file '%s'", self.relpath)
|
logger.info("Generating lock file '%s'", self.relpath)
|
||||||
|
|
||||||
@@ -436,8 +398,7 @@ class Lockfile(FileMixin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
d, _ = self._load_yaml(round_trip=True)
|
d, _ = self._load_yaml(round_trip=True)
|
||||||
version = LOCKFILE_VERSION.from_dict(d)
|
data = d.get("stages", {})
|
||||||
data = d if version == LOCKFILE_VERSION.V1 else d.get("stages", {})
|
|
||||||
if stage.name not in data:
|
if stage.name not in data:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import enum
|
|
||||||
from collections.abc import Mapping
|
|
||||||
|
|
||||||
from voluptuous import validators
|
|
||||||
|
|
||||||
SCHEMA_KWD = "schema"
|
|
||||||
META_KWD = "meta"
|
|
||||||
|
|
||||||
|
|
||||||
def lockfile_version_schema(value):
|
|
||||||
expected = [LOCKFILE_VERSION.V2.value] # pylint: disable=no-member
|
|
||||||
msg = f"invalid schema version {value}, expected one of {expected}"
|
|
||||||
return validators.Any(*expected, msg=msg)(value)
|
|
||||||
|
|
||||||
|
|
||||||
class VersionEnum(str, enum.Enum):
|
|
||||||
@classmethod
|
|
||||||
def all_versions(cls):
|
|
||||||
return [v.value for v in cls]
|
|
||||||
|
|
||||||
|
|
||||||
class LOCKFILE_VERSION(VersionEnum): # noqa: N801
|
|
||||||
V1 = "1.0"
|
|
||||||
V2 = "2.0"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
# 1) if it's empty or or is not a dict, use the latest one (V2).
|
|
||||||
# 2) use the `schema` identifier if it exists and is a supported
|
|
||||||
# version
|
|
||||||
# 3) if it's not in any of the supported version, use the latest one
|
|
||||||
# 4) if there's no identifier, it's a V1
|
|
||||||
if not data or not isinstance(data, Mapping):
|
|
||||||
return cls(cls.V2)
|
|
||||||
|
|
||||||
version = data.get(SCHEMA_KWD)
|
|
||||||
if version:
|
|
||||||
return cls(version if version in cls.all_versions() else cls.V2)
|
|
||||||
return cls(cls.V1)
|
|
||||||
+4
-8
@@ -1,6 +1,6 @@
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
|
||||||
from voluptuous import Any, Optional, Required, Schema
|
from voluptuous import Any, Equal, Optional, Required, Schema
|
||||||
|
|
||||||
from dvc import dependency, output
|
from dvc import dependency, output
|
||||||
from dvc.annotations import ANNOTATION_SCHEMA, ARTIFACT_SCHEMA
|
from dvc.annotations import ANNOTATION_SCHEMA, ARTIFACT_SCHEMA
|
||||||
@@ -12,7 +12,6 @@ from dvc.output import (
|
|||||||
Output,
|
Output,
|
||||||
)
|
)
|
||||||
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
|
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
|
||||||
from dvc.parsing.versions import SCHEMA_KWD, lockfile_version_schema
|
|
||||||
from dvc.stage.params import StageParams
|
from dvc.stage.params import StageParams
|
||||||
|
|
||||||
STAGES = "stages"
|
STAGES = "stages"
|
||||||
@@ -44,11 +43,9 @@ LOCK_FILE_STAGE_SCHEMA = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOCKFILE_STAGES_SCHEMA = {str: LOCK_FILE_STAGE_SCHEMA}
|
LOCKFILE_STAGES_SCHEMA = {str: LOCK_FILE_STAGE_SCHEMA}
|
||||||
|
LOCKFILE_SCHEMA = {
|
||||||
LOCKFILE_V1_SCHEMA = LOCKFILE_STAGES_SCHEMA
|
Required("schema"): Equal("2.0", "invalid schema version"),
|
||||||
LOCKFILE_V2_SCHEMA = {
|
|
||||||
STAGES: LOCKFILE_STAGES_SCHEMA,
|
STAGES: LOCKFILE_STAGES_SCHEMA,
|
||||||
Required(SCHEMA_KWD): lockfile_version_schema,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OUT_PSTAGE_DETAILED_SCHEMA = {
|
OUT_PSTAGE_DETAILED_SCHEMA = {
|
||||||
@@ -135,5 +132,4 @@ MULTI_STAGE_SCHEMA = {
|
|||||||
COMPILED_SINGLE_STAGE_SCHEMA = Schema(SINGLE_STAGE_SCHEMA)
|
COMPILED_SINGLE_STAGE_SCHEMA = Schema(SINGLE_STAGE_SCHEMA)
|
||||||
COMPILED_MULTI_STAGE_SCHEMA = Schema(MULTI_STAGE_SCHEMA)
|
COMPILED_MULTI_STAGE_SCHEMA = Schema(MULTI_STAGE_SCHEMA)
|
||||||
COMPILED_LOCK_FILE_STAGE_SCHEMA = Schema(LOCK_FILE_STAGE_SCHEMA)
|
COMPILED_LOCK_FILE_STAGE_SCHEMA = Schema(LOCK_FILE_STAGE_SCHEMA)
|
||||||
COMPILED_LOCKFILE_V1_SCHEMA = Schema(LOCKFILE_V1_SCHEMA)
|
COMPILED_LOCKFILE_SCHEMA = Schema(LOCKFILE_SCHEMA)
|
||||||
COMPILED_LOCKFILE_V2_SCHEMA = Schema(LOCKFILE_V2_SCHEMA)
|
|
||||||
|
|||||||
+1
-6
@@ -8,7 +8,6 @@ from funcy import get_in, lcat, once, project
|
|||||||
|
|
||||||
from dvc import dependency, output
|
from dvc import dependency, output
|
||||||
from dvc.parsing import FOREACH_KWD, JOIN, EntryNotFound
|
from dvc.parsing import FOREACH_KWD, JOIN, EntryNotFound
|
||||||
from dvc.parsing.versions import LOCKFILE_VERSION
|
|
||||||
from dvc.utils.objects import cached_property
|
from dvc.utils.objects import cached_property
|
||||||
from dvc_data.hashfile.hash_info import HashInfo
|
from dvc_data.hashfile.hash_info import HashInfo
|
||||||
from dvc_data.hashfile.meta import Meta
|
from dvc_data.hashfile.meta import Meta
|
||||||
@@ -39,11 +38,7 @@ class StageLoader(Mapping):
|
|||||||
self.repo = self.dvcfile.repo
|
self.repo = self.dvcfile.repo
|
||||||
|
|
||||||
lockfile_data = lockfile_data or {}
|
lockfile_data = lockfile_data or {}
|
||||||
version = LOCKFILE_VERSION.from_dict(lockfile_data)
|
self._lockfile_data = lockfile_data.get("stages", {})
|
||||||
if version == LOCKFILE_VERSION.V1:
|
|
||||||
self._lockfile_data = lockfile_data
|
|
||||||
else:
|
|
||||||
self._lockfile_data = lockfile_data.get("stages", {})
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def lockfile_data(self) -> Dict[str, Any]:
|
def lockfile_data(self) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
import pytest
|
from dvc.dvcfile import LOCK_FILE
|
||||||
|
|
||||||
from dvc.dvcfile import LOCK_FILE, Lockfile
|
|
||||||
from dvc.stage.utils import split_params_deps
|
from dvc.stage.utils import split_params_deps
|
||||||
from dvc.utils.fs import remove
|
from dvc.utils.fs import remove
|
||||||
from dvc.utils.serialize import dumps_yaml, parse_yaml_for_update
|
from dvc.utils.serialize import dumps_yaml, parse_yaml_for_update
|
||||||
from dvc.utils.strictyaml import YAMLValidationError, make_relpath
|
|
||||||
from dvc_data.hashfile.hash_info import HashInfo
|
|
||||||
from tests.func.test_run_multistage import supported_params
|
from tests.func.test_run_multistage import supported_params
|
||||||
|
|
||||||
FS_STRUCTURE = {
|
FS_STRUCTURE = {
|
||||||
@@ -157,57 +151,3 @@ def test_params_dump(tmp_dir, dvc, run_head):
|
|||||||
remove(item)
|
remove(item)
|
||||||
assert dvc.reproduce(stage.addressing) == [stage]
|
assert dvc.reproduce(stage.addressing) == [stage]
|
||||||
assert_eq_lockfile(initial_content, read_lock_file())
|
assert_eq_lockfile(initial_content, read_lock_file())
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def v1_repo_lock(tmp_dir, dvc):
|
|
||||||
"""Generates a repo having v1 format lockfile"""
|
|
||||||
size = 5 if os.name == "nt" else 4
|
|
||||||
hi = HashInfo(name="md5", value="c157a79031e1c40f85931829bc5fc552")
|
|
||||||
v1_lockdata = {
|
|
||||||
"foo": {"cmd": "echo foo"},
|
|
||||||
"bar": {
|
|
||||||
"cmd": "echo bar>bar.txt",
|
|
||||||
"outs": [{"path": "bar.txt", **hi.to_dict(), "size": size}],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dvc.run(cmd="echo foo", name="foo", no_exec=True)
|
|
||||||
dvc.run(cmd="echo bar>bar.txt", outs=["bar.txt"], name="bar", no_exec=True)
|
|
||||||
(tmp_dir / "dvc.lock").dump(v1_lockdata)
|
|
||||||
return v1_lockdata
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_read_v1_lockfile(tmp_dir, dvc, v1_repo_lock):
|
|
||||||
assert dvc.status() == {
|
|
||||||
"bar": [{"changed outs": {"bar.txt": "not in cache"}}],
|
|
||||||
"foo": ["always changed"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_migrates_v1_lockfile_to_v2_during_dump(tmp_dir, dvc, v1_repo_lock, caplog):
|
|
||||||
caplog.clear()
|
|
||||||
with caplog.at_level(logging.INFO, logger="dvc.dvcfile"):
|
|
||||||
assert dvc.reproduce()
|
|
||||||
|
|
||||||
assert "Migrating lock file 'dvc.lock' from v1 to v2" in caplog.messages
|
|
||||||
d = (tmp_dir / "dvc.lock").parse()
|
|
||||||
assert d == {"stages": v1_repo_lock, "schema": "2.0"}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"version_info", [{"schema": "1.1"}, {"schema": "2.1"}, {"schema": "3.0"}]
|
|
||||||
)
|
|
||||||
def test_lockfile_invalid_versions(tmp_dir, dvc, version_info):
|
|
||||||
lockdata = {**version_info, "stages": {"foo": {"cmd": "echo foo"}}}
|
|
||||||
(tmp_dir / "dvc.lock").dump(lockdata)
|
|
||||||
with pytest.raises(YAMLValidationError) as exc_info:
|
|
||||||
Lockfile(dvc, (tmp_dir / "dvc.lock").fs_path).load()
|
|
||||||
|
|
||||||
rel = make_relpath("dvc.lock")
|
|
||||||
assert f"'{rel}' validation failed" in str(exc_info.value)
|
|
||||||
assert (
|
|
||||||
str(exc_info.value.__cause__)
|
|
||||||
== f"invalid schema version {version_info['schema']}, "
|
|
||||||
"expected one of ['2.0'] for dictionary value @ "
|
|
||||||
"data['schema']"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def test_stage_dump_no_outs_deps(tmp_dir, dvc):
|
|||||||
|
|
||||||
def test_stage_dump_when_already_exists(tmp_dir, dvc):
|
def test_stage_dump_when_already_exists(tmp_dir, dvc):
|
||||||
data = {"s1": {"cmd": "command", "deps": [], "outs": []}}
|
data = {"s1": {"cmd": "command", "deps": [], "outs": []}}
|
||||||
(tmp_dir / "path.lock").dump(data)
|
(tmp_dir / "path.lock").dump({"schema": "2.0", "stages": data})
|
||||||
stage = PipelineStage(name="s2", repo=dvc, path="path", cmd="command2")
|
stage = PipelineStage(name="s2", repo=dvc, path="path", cmd="command2")
|
||||||
lockfile = Lockfile(dvc, "path.lock")
|
lockfile = Lockfile(dvc, "path.lock")
|
||||||
lockfile.dump(stage)
|
lockfile.dump(stage)
|
||||||
@@ -35,7 +35,7 @@ def test_stage_dump_with_deps_and_outs(tmp_dir, dvc):
|
|||||||
"outs": [{"md5": "2.txt", "path": "checksum"}],
|
"outs": [{"md5": "2.txt", "path": "checksum"}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(tmp_dir / "path.lock").dump(data)
|
(tmp_dir / "path.lock").dump({"schema": "2.0", "stages": data})
|
||||||
lockfile = Lockfile(dvc, "path.lock")
|
lockfile = Lockfile(dvc, "path.lock")
|
||||||
stage = PipelineStage(name="s2", repo=dvc, path="path", cmd="command2")
|
stage = PipelineStage(name="s2", repo=dvc, path="path", cmd="command2")
|
||||||
lockfile.dump(stage)
|
lockfile.dump(stage)
|
||||||
|
|||||||
Reference in New Issue
Block a user