We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
remove: remove dvc.yaml and dvc.lock if they are empty
https://github.com/iterative/dvc/pull/4074#issuecomment-648097445

```
$ cat dvc.lock
{}                                               
$ cat dvc.yaml
stages: {}                                       
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `dvc/dvcfile.py`
Content:
```
1 import collections
2 import contextlib
3 import logging
4 import os
5 
6 from voluptuous import MultipleInvalid
7 
8 from dvc.exceptions import DvcException
9 from dvc.stage import serialize
10 from dvc.stage.exceptions import (
11     StageFileBadNameError,
12     StageFileDoesNotExistError,
13     StageFileFormatError,
14     StageFileIsNotDvcFileError,
15 )
16 from dvc.stage.loader import SingleStageLoader, StageLoader
17 from dvc.utils import relpath
18 from dvc.utils.collections import apply_diff
19 from dvc.utils.yaml import dump_yaml, parse_yaml, parse_yaml_for_update
20 
21 logger = logging.getLogger(__name__)
22 
23 DVC_FILE = "Dvcfile"
24 DVC_FILE_SUFFIX = ".dvc"
25 PIPELINE_FILE = "dvc.yaml"
26 PIPELINE_LOCK = "dvc.lock"
27 
28 
29 class LockfileCorruptedError(DvcException):
30     pass
31 
32 
33 def is_valid_filename(path):
34     return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [
35         DVC_FILE,
36         PIPELINE_FILE,
37     ]
38 
39 
40 def is_dvc_file(path):
41     return os.path.isfile(path) and (
42         is_valid_filename(path) or os.path.basename(path) == PIPELINE_LOCK
43     )
44 
45 
46 def check_dvc_filename(path):
47     if not is_valid_filename(path):
48         raise StageFileBadNameError(
49             "bad DVC-file name '{}'. DVC-files should be named "
50             "'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
51                 relpath(path), os.path.basename(path)
52             )
53         )
54 
55 
56 class FileMixin:
57     SCHEMA = None
58 
59     def __init__(self, repo, path, **kwargs):
60         self.repo = repo
61         self.path = path
62 
63     def __repr__(self):
64         return "{}: {}".format(
65             self.__class__.__name__, relpath(self.path, self.repo.root_dir)
66         )
67 
68     def __hash__(self):
69         return hash(self.path)
70 
71     def __eq__(self, other):
72         return self.repo == other.repo and os.path.abspath(
73             self.path
74         ) == os.path.abspath(other.path)
75 
76     def __str__(self):
77         return f"{self.__class__.__name__}: {self.relpath}"
78 
79     @property
80     def relpath(self):
81         return relpath(self.path)
82 
83     def exists(self):
84         return self.repo.tree.exists(self.path)
85 
86     def _load(self):
87         # it raises the proper exceptions by priority:
88         # 1. when the file doesn't exists
89         # 2. filename is not a DVC-file
90         # 3. path doesn't represent a regular file
91         if not self.exists():
92             raise StageFileDoesNotExistError(self.path)
93         check_dvc_filename(self.path)
94         if not self.repo.tree.isfile(self.path):
95             raise StageFileIsNotDvcFileError(self.path)
96 
97         with self.repo.tree.open(self.path) as fd:
98             stage_text = fd.read()
99         d = parse_yaml(stage_text, self.path)
100         self.validate(d, self.relpath)
101         return d, stage_text
102 
103     @classmethod
104     def validate(cls, d, fname=None):
105         assert isinstance(cls.SCHEMA, collections.abc.Callable)
106         try:
107             cls.SCHEMA(d)  # pylint: disable=not-callable
108         except MultipleInvalid as exc:
109             raise StageFileFormatError(f"'{fname}' format error: {exc}")
110 
111     def remove(self, force=False):  # pylint: disable=unused-argument
112         with contextlib.suppress(FileNotFoundError):
113             os.unlink(self.path)
114 
115     def dump(self, stage, **kwargs):
116         raise NotImplementedError
117 
118 
119 class SingleStageFile(FileMixin):
120     from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA
121 
122     @property
123     def stage(self):
124         data, raw = self._load()
125         return SingleStageLoader.load_stage(self, data, raw)
126 
127     @property
128     def stages(self):
129         data, raw = self._load()
130         return SingleStageLoader(self, data, raw)
131 
132     def dump(self, stage, **kwargs):
133         """Dumps given stage appropriately in the dvcfile."""
134         from dvc.stage import PipelineStage
135 
136         assert not isinstance(stage, PipelineStage)
137         check_dvc_filename(self.path)
138         logger.debug(
139             "Saving information to '{file}'.".format(file=relpath(self.path))
140         )
141         dump_yaml(self.path, serialize.to_single_stage_file(stage))
142         self.repo.scm.track_file(self.relpath)
143 
144     def remove_stage(self, stage):  # pylint: disable=unused-argument
145         self.remove()
146 
147 
148 class PipelineFile(FileMixin):
149     """Abstraction for pipelines file, .yaml + .lock combined."""
150 
151     from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA
152 
153     @property
154     def _lockfile(self):
155         return Lockfile(self.repo, os.path.splitext(self.path)[0] + ".lock")
156 
157     def dump(
158         self, stage, update_pipeline=False, no_lock=False, **kwargs
159     ):  # pylint: disable=arguments-differ
160         """Dumps given stage appropriately in the dvcfile."""
161         from dvc.stage import PipelineStage
162 
163         assert isinstance(stage, PipelineStage)
164         check_dvc_filename(self.path)
165 
166         if update_pipeline and not stage.is_data_source:
167             self._dump_pipeline_file(stage)
168 
169         if not no_lock:
170             self._dump_lockfile(stage)
171 
172     def _dump_lockfile(self, stage):
173         self._lockfile.dump(stage)
174 
175     def _dump_pipeline_file(self, stage):
176         data = {}
177         if self.exists():
178             with open(self.path) as fd:
179                 data = parse_yaml_for_update(fd.read(), self.path)
180         else:
181             logger.info("Creating '%s'", self.relpath)
182             open(self.path, "w+").close()
183 
184         data["stages"] = data.get("stages", {})
185         stage_data = serialize.to_pipeline_file(stage)
186         existing_entry = stage.name in data["stages"]
187 
188         action = "Modifying" if existing_entry else "Adding"
189         logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
190 
191         if existing_entry:
192             orig_stage_data = data["stages"][stage.name]
193             if "meta" in orig_stage_data:
194                 stage_data[stage.name]["meta"] = orig_stage_data["meta"]
195             apply_diff(stage_data[stage.name], orig_stage_data)
196         else:
197             data["stages"].update(stage_data)
198 
199         dump_yaml(self.path, data)
200         self.repo.scm.track_file(self.relpath)
201 
202     @property
203     def stage(self):
204         raise DvcException(
205             "PipelineFile has multiple stages. Please specify it's name."
206         )
207 
208     @property
209     def stages(self):
210         data, _ = self._load()
211         lockfile_data = self._lockfile.load()
212         return StageLoader(self, data.get("stages", {}), lockfile_data)
213 
214     def remove(self, force=False):
215         if not force:
216             logger.warning("Cannot remove pipeline file.")
217             return
218 
219         super().remove()
220         self._lockfile.remove()
221 
222     def remove_stage(self, stage):
223         self._lockfile.remove_stage(stage)
224         if not self.exists():
225             return
226 
227         with open(self.path, "r") as f:
228             d = parse_yaml_for_update(f.read(), self.path)
229 
230         self.validate(d, self.path)
231         if stage.name not in d.get("stages", {}):
232             return
233 
234         logger.debug("Removing '%s' from '%s'", stage.name, self.path)
235         del d["stages"][stage.name]
236         dump_yaml(self.path, d)
237 
238 
239 class Lockfile(FileMixin):
240     from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
241 
242     def load(self):
243         if not self.exists():
244             return {}
245         with self.repo.tree.open(self.path) as fd:
246             data = parse_yaml(fd.read(), self.path)
247         try:
248             self.validate(data, fname=self.relpath)
249         except StageFileFormatError:
250             raise LockfileCorruptedError(
251                 f"Lockfile '{self.relpath}' is corrupted."
252             )
253         return data
254 
255     def dump(self, stage, **kwargs):
256         stage_data = serialize.to_lockfile(stage)
257         if not self.exists():
258             modified = True
259             logger.info("Generating lock file '%s'", self.relpath)
260             data = stage_data
261             open(self.path, "w+").close()
262         else:
263             with self.repo.tree.open(self.path, "r") as fd:
264                 data = parse_yaml_for_update(fd.read(), self.path)
265             modified = data.get(stage.name, {}) != stage_data.get(
266                 stage.name, {}
267             )
268             if modified:
269                 logger.info("Updating lock file '%s'", self.relpath)
270             data.update(stage_data)
271         dump_yaml(self.path, data)
272         if modified:
273             self.repo.scm.track_file(self.relpath)
274 
275     def remove_stage(self, stage):
276         if not self.exists():
277             return
278 
279         with open(self.path) as f:
280             d = parse_yaml_for_update(f.read(), self.path)
281         self.validate(d, self.path)
282 
283         if stage.name not in d:
284             return
285 
286         logger.debug("Removing '%s' from '%s'", stage.name, self.path)
287         del d[stage.name]
288 
289         dump_yaml(self.path, d)
290 
291 
292 class Dvcfile:
293     def __new__(cls, repo, path, **kwargs):
294         assert path
295         assert repo
296 
297         _, ext = os.path.splitext(path)
298         if ext in [".yaml", ".yml"]:
299             return PipelineFile(repo, path, **kwargs)
300         # fallback to single stage file for better error messages
301         return SingleStageFile(repo, path, **kwargs)
302
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/dvc/dvcfile.py b/dvc/dvcfile.py
--- a/dvc/dvcfile.py
+++ b/dvc/dvcfile.py
@@ -233,7 +233,11 @@
 
         logger.debug("Removing '%s' from '%s'", stage.name, self.path)
         del d["stages"][stage.name]
-        dump_yaml(self.path, d)
+
+        if d["stages"]:
+            dump_yaml(self.path, d)
+        else:
+            super().remove()
 
 
 class Lockfile(FileMixin):
@@ -286,7 +290,10 @@
         logger.debug("Removing '%s' from '%s'", stage.name, self.path)
         del d[stage.name]
 
-        dump_yaml(self.path, d)
+        if d:
+            dump_yaml(self.path, d)
+        else:
+            self.remove()
 
 
 class Dvcfile:

{"golden_diff": "diff --git a/dvc/dvcfile.py b/dvc/dvcfile.py\n--- a/dvc/dvcfile.py\n+++ b/dvc/dvcfile.py\n@@ -233,7 +233,11 @@\n \n         logger.debug(\"Removing '%s' from '%s'\", stage.name, self.path)\n         del d[\"stages\"][stage.name]\n-        dump_yaml(self.path, d)\n+\n+        if d[\"stages\"]:\n+            dump_yaml(self.path, d)\n+        else:\n+            super().remove()\n \n \n class Lockfile(FileMixin):\n@@ -286,7 +290,10 @@\n         logger.debug(\"Removing '%s' from '%s'\", stage.name, self.path)\n         del d[stage.name]\n \n-        dump_yaml(self.path, d)\n+        if d:\n+            dump_yaml(self.path, d)\n+        else:\n+            self.remove()\n \n \n class Dvcfile:\n", "issue": "remove: remove dvc.yaml and dvc.lock if they are empty\nhttps://github.com/iterative/dvc/pull/4074#issuecomment-648097445\r\n\r\n```\r\n$ cat dvc.lock\r\n{}                                               \r\n$ cat dvc.yaml\r\nstages: {}                                       \r\n```\n", "before_files": [{"content": "import collections\nimport contextlib\nimport logging\nimport os\n\nfrom voluptuous import MultipleInvalid\n\nfrom dvc.exceptions import DvcException\nfrom dvc.stage import serialize\nfrom dvc.stage.exceptions import (\n    StageFileBadNameError,\n    StageFileDoesNotExistError,\n    StageFileFormatError,\n    StageFileIsNotDvcFileError,\n)\nfrom dvc.stage.loader import SingleStageLoader, StageLoader\nfrom dvc.utils import relpath\nfrom dvc.utils.collections import apply_diff\nfrom dvc.utils.yaml import dump_yaml, parse_yaml, parse_yaml_for_update\n\nlogger = logging.getLogger(__name__)\n\nDVC_FILE = \"Dvcfile\"\nDVC_FILE_SUFFIX = \".dvc\"\nPIPELINE_FILE = \"dvc.yaml\"\nPIPELINE_LOCK = \"dvc.lock\"\n\n\nclass LockfileCorruptedError(DvcException):\n    pass\n\n\ndef is_valid_filename(path):\n    return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [\n        DVC_FILE,\n        PIPELINE_FILE,\n    ]\n\n\ndef is_dvc_file(path):\n    return os.path.isfile(path) and (\n        is_valid_filename(path) or os.path.basename(path) == PIPELINE_LOCK\n    )\n\n\ndef check_dvc_filename(path):\n    if not is_valid_filename(path):\n        raise StageFileBadNameError(\n            \"bad DVC-file name '{}'. DVC-files should be named \"\n            \"'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').\".format(\n                relpath(path), os.path.basename(path)\n            )\n        )\n\n\nclass FileMixin:\n    SCHEMA = None\n\n    def __init__(self, repo, path, **kwargs):\n        self.repo = repo\n        self.path = path\n\n    def __repr__(self):\n        return \"{}: {}\".format(\n            self.__class__.__name__, relpath(self.path, self.repo.root_dir)\n        )\n\n    def __hash__(self):\n        return hash(self.path)\n\n    def __eq__(self, other):\n        return self.repo == other.repo and os.path.abspath(\n            self.path\n        ) == os.path.abspath(other.path)\n\n    def __str__(self):\n        return f\"{self.__class__.__name__}: {self.relpath}\"\n\n    @property\n    def relpath(self):\n        return relpath(self.path)\n\n    def exists(self):\n        return self.repo.tree.exists(self.path)\n\n    def _load(self):\n        # it raises the proper exceptions by priority:\n        # 1. when the file doesn't exists\n        # 2. filename is not a DVC-file\n        # 3. path doesn't represent a regular file\n        if not self.exists():\n            raise StageFileDoesNotExistError(self.path)\n        check_dvc_filename(self.path)\n        if not self.repo.tree.isfile(self.path):\n            raise StageFileIsNotDvcFileError(self.path)\n\n        with self.repo.tree.open(self.path) as fd:\n            stage_text = fd.read()\n        d = parse_yaml(stage_text, self.path)\n        self.validate(d, self.relpath)\n        return d, stage_text\n\n    @classmethod\n    def validate(cls, d, fname=None):\n        assert isinstance(cls.SCHEMA, collections.abc.Callable)\n        try:\n            cls.SCHEMA(d)  # pylint: disable=not-callable\n        except MultipleInvalid as exc:\n            raise StageFileFormatError(f\"'{fname}' format error: {exc}\")\n\n    def remove(self, force=False):  # pylint: disable=unused-argument\n        with contextlib.suppress(FileNotFoundError):\n            os.unlink(self.path)\n\n    def dump(self, stage, **kwargs):\n        raise NotImplementedError\n\n\nclass SingleStageFile(FileMixin):\n    from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA\n\n    @property\n    def stage(self):\n        data, raw = self._load()\n        return SingleStageLoader.load_stage(self, data, raw)\n\n    @property\n    def stages(self):\n        data, raw = self._load()\n        return SingleStageLoader(self, data, raw)\n\n    def dump(self, stage, **kwargs):\n        \"\"\"Dumps given stage appropriately in the dvcfile.\"\"\"\n        from dvc.stage import PipelineStage\n\n        assert not isinstance(stage, PipelineStage)\n        check_dvc_filename(self.path)\n        logger.debug(\n            \"Saving information to '{file}'.\".format(file=relpath(self.path))\n        )\n        dump_yaml(self.path, serialize.to_single_stage_file(stage))\n        self.repo.scm.track_file(self.relpath)\n\n    def remove_stage(self, stage):  # pylint: disable=unused-argument\n        self.remove()\n\n\nclass PipelineFile(FileMixin):\n    \"\"\"Abstraction for pipelines file, .yaml + .lock combined.\"\"\"\n\n    from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA\n\n    @property\n    def _lockfile(self):\n        return Lockfile(self.repo, os.path.splitext(self.path)[0] + \".lock\")\n\n    def dump(\n        self, stage, update_pipeline=False, no_lock=False, **kwargs\n    ):  # pylint: disable=arguments-differ\n        \"\"\"Dumps given stage appropriately in the dvcfile.\"\"\"\n        from dvc.stage import PipelineStage\n\n        assert isinstance(stage, PipelineStage)\n        check_dvc_filename(self.path)\n\n        if update_pipeline and not stage.is_data_source:\n            self._dump_pipeline_file(stage)\n\n        if not no_lock:\n            self._dump_lockfile(stage)\n\n    def _dump_lockfile(self, stage):\n        self._lockfile.dump(stage)\n\n    def _dump_pipeline_file(self, stage):\n        data = {}\n        if self.exists():\n            with open(self.path) as fd:\n                data = parse_yaml_for_update(fd.read(), self.path)\n        else:\n            logger.info(\"Creating '%s'\", self.relpath)\n            open(self.path, \"w+\").close()\n\n        data[\"stages\"] = data.get(\"stages\", {})\n        stage_data = serialize.to_pipeline_file(stage)\n        existing_entry = stage.name in data[\"stages\"]\n\n        action = \"Modifying\" if existing_entry else \"Adding\"\n        logger.info(\"%s stage '%s' in '%s'\", action, stage.name, self.relpath)\n\n        if existing_entry:\n            orig_stage_data = data[\"stages\"][stage.name]\n            if \"meta\" in orig_stage_data:\n                stage_data[stage.name][\"meta\"] = orig_stage_data[\"meta\"]\n            apply_diff(stage_data[stage.name], orig_stage_data)\n        else:\n            data[\"stages\"].update(stage_data)\n\n        dump_yaml(self.path, data)\n        self.repo.scm.track_file(self.relpath)\n\n    @property\n    def stage(self):\n        raise DvcException(\n            \"PipelineFile has multiple stages. Please specify it's name.\"\n        )\n\n    @property\n    def stages(self):\n        data, _ = self._load()\n        lockfile_data = self._lockfile.load()\n        return StageLoader(self, data.get(\"stages\", {}), lockfile_data)\n\n    def remove(self, force=False):\n        if not force:\n            logger.warning(\"Cannot remove pipeline file.\")\n            return\n\n        super().remove()\n        self._lockfile.remove()\n\n    def remove_stage(self, stage):\n        self._lockfile.remove_stage(stage)\n        if not self.exists():\n            return\n\n        with open(self.path, \"r\") as f:\n            d = parse_yaml_for_update(f.read(), self.path)\n\n        self.validate(d, self.path)\n        if stage.name not in d.get(\"stages\", {}):\n            return\n\n        logger.debug(\"Removing '%s' from '%s'\", stage.name, self.path)\n        del d[\"stages\"][stage.name]\n        dump_yaml(self.path, d)\n\n\nclass Lockfile(FileMixin):\n    from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA\n\n    def load(self):\n        if not self.exists():\n            return {}\n        with self.repo.tree.open(self.path) as fd:\n            data = parse_yaml(fd.read(), self.path)\n        try:\n            self.validate(data, fname=self.relpath)\n        except StageFileFormatError:\n            raise LockfileCorruptedError(\n                f\"Lockfile '{self.relpath}' is corrupted.\"\n            )\n        return data\n\n    def dump(self, stage, **kwargs):\n        stage_data = serialize.to_lockfile(stage)\n        if not self.exists():\n            modified = True\n            logger.info(\"Generating lock file '%s'\", self.relpath)\n            data = stage_data\n            open(self.path, \"w+\").close()\n        else:\n            with self.repo.tree.open(self.path, \"r\") as fd:\n                data = parse_yaml_for_update(fd.read(), self.path)\n            modified = data.get(stage.name, {}) != stage_data.get(\n                stage.name, {}\n            )\n            if modified:\n                logger.info(\"Updating lock file '%s'\", self.relpath)\n            data.update(stage_data)\n        dump_yaml(self.path, data)\n        if modified:\n            self.repo.scm.track_file(self.relpath)\n\n    def remove_stage(self, stage):\n        if not self.exists():\n            return\n\n        with open(self.path) as f:\n            d = parse_yaml_for_update(f.read(), self.path)\n        self.validate(d, self.path)\n\n        if stage.name not in d:\n            return\n\n        logger.debug(\"Removing '%s' from '%s'\", stage.name, self.path)\n        del d[stage.name]\n\n        dump_yaml(self.path, d)\n\n\nclass Dvcfile:\n    def __new__(cls, repo, path, **kwargs):\n        assert path\n        assert repo\n\n        _, ext = os.path.splitext(path)\n        if ext in [\".yaml\", \".yml\"]:\n            return PipelineFile(repo, path, **kwargs)\n        # fallback to single stage file for better error messages\n        return SingleStageFile(repo, path, **kwargs)\n", "path": "dvc/dvcfile.py"}], "after_files": [{"content": "import collections\nimport contextlib\nimport logging\nimport os\n\nfrom voluptuous import MultipleInvalid\n\nfrom dvc.exceptions import DvcException\nfrom dvc.stage import serialize\nfrom dvc.stage.exceptions import (\n    StageFileBadNameError,\n    StageFileDoesNotExistError,\n    StageFileFormatError,\n    StageFileIsNotDvcFileError,\n)\nfrom dvc.stage.loader import SingleStageLoader, StageLoader\nfrom dvc.utils import relpath\nfrom dvc.utils.collections import apply_diff\nfrom dvc.utils.yaml import dump_yaml, parse_yaml, parse_yaml_for_update\n\nlogger = logging.getLogger(__name__)\n\nDVC_FILE = \"Dvcfile\"\nDVC_FILE_SUFFIX = \".dvc\"\nPIPELINE_FILE = \"dvc.yaml\"\nPIPELINE_LOCK = \"dvc.lock\"\n\n\nclass LockfileCorruptedError(DvcException):\n    pass\n\n\ndef is_valid_filename(path):\n    return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [\n        DVC_FILE,\n        PIPELINE_FILE,\n    ]\n\n\ndef is_dvc_file(path):\n    return os.path.isfile(path) and (\n        is_valid_filename(path) or os.path.basename(path) == PIPELINE_LOCK\n    )\n\n\ndef check_dvc_filename(path):\n    if not is_valid_filename(path):\n        raise StageFileBadNameError(\n            \"bad DVC-file name '{}'. DVC-files should be named \"\n            \"'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').\".format(\n                relpath(path), os.path.basename(path)\n            )\n        )\n\n\nclass FileMixin:\n    SCHEMA = None\n\n    def __init__(self, repo, path, **kwargs):\n        self.repo = repo\n        self.path = path\n\n    def __repr__(self):\n        return \"{}: {}\".format(\n            self.__class__.__name__, relpath(self.path, self.repo.root_dir)\n        )\n\n    def __hash__(self):\n        return hash(self.path)\n\n    def __eq__(self, other):\n        return self.repo == other.repo and os.path.abspath(\n            self.path\n        ) == os.path.abspath(other.path)\n\n    def __str__(self):\n        return f\"{self.__class__.__name__}: {self.relpath}\"\n\n    @property\n    def relpath(self):\n        return relpath(self.path)\n\n    def exists(self):\n        return self.repo.tree.exists(self.path)\n\n    def _load(self):\n        # it raises the proper exceptions by priority:\n        # 1. when the file doesn't exists\n        # 2. filename is not a DVC-file\n        # 3. path doesn't represent a regular file\n        if not self.exists():\n            raise StageFileDoesNotExistError(self.path)\n        check_dvc_filename(self.path)\n        if not self.repo.tree.isfile(self.path):\n            raise StageFileIsNotDvcFileError(self.path)\n\n        with self.repo.tree.open(self.path) as fd:\n            stage_text = fd.read()\n        d = parse_yaml(stage_text, self.path)\n        self.validate(d, self.relpath)\n        return d, stage_text\n\n    @classmethod\n    def validate(cls, d, fname=None):\n        assert isinstance(cls.SCHEMA, collections.abc.Callable)\n        try:\n            cls.SCHEMA(d)  # pylint: disable=not-callable\n        except MultipleInvalid as exc:\n            raise StageFileFormatError(f\"'{fname}' format error: {exc}\")\n\n    def remove(self, force=False):  # pylint: disable=unused-argument\n        with contextlib.suppress(FileNotFoundError):\n            os.unlink(self.path)\n\n    def dump(self, stage, **kwargs):\n        raise NotImplementedError\n\n\nclass SingleStageFile(FileMixin):\n    from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA\n\n    @property\n    def stage(self):\n        data, raw = self._load()\n        return SingleStageLoader.load_stage(self, data, raw)\n\n    @property\n    def stages(self):\n        data, raw = self._load()\n        return SingleStageLoader(self, data, raw)\n\n    def dump(self, stage, **kwargs):\n        \"\"\"Dumps given stage appropriately in the dvcfile.\"\"\"\n        from dvc.stage import PipelineStage\n\n        assert not isinstance(stage, PipelineStage)\n        check_dvc_filename(self.path)\n        logger.debug(\n            \"Saving information to '{file}'.\".format(file=relpath(self.path))\n        )\n        dump_yaml(self.path, serialize.to_single_stage_file(stage))\n        self.repo.scm.track_file(self.relpath)\n\n    def remove_stage(self, stage):  # pylint: disable=unused-argument\n        self.remove()\n\n\nclass PipelineFile(FileMixin):\n    \"\"\"Abstraction for pipelines file, .yaml + .lock combined.\"\"\"\n\n    from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA\n\n    @property\n    def _lockfile(self):\n        return Lockfile(self.repo, os.path.splitext(self.path)[0] + \".lock\")\n\n    def dump(\n        self, stage, update_pipeline=False, no_lock=False, **kwargs\n    ):  # pylint: disable=arguments-differ\n        \"\"\"Dumps given stage appropriately in the dvcfile.\"\"\"\n        from dvc.stage import PipelineStage\n\n        assert isinstance(stage, PipelineStage)\n        check_dvc_filename(self.path)\n\n        if update_pipeline and not stage.is_data_source:\n            self._dump_pipeline_file(stage)\n\n        if not no_lock:\n            self._dump_lockfile(stage)\n\n    def _dump_lockfile(self, stage):\n        self._lockfile.dump(stage)\n\n    def _dump_pipeline_file(self, stage):\n        data = {}\n        if self.exists():\n            with open(self.path) as fd:\n                data = parse_yaml_for_update(fd.read(), self.path)\n        else:\n            logger.info(\"Creating '%s'\", self.relpath)\n            open(self.path, \"w+\").close()\n\n        data[\"stages\"] = data.get(\"stages\", {})\n        stage_data = serialize.to_pipeline_file(stage)\n        existing_entry = stage.name in data[\"stages\"]\n\n        action = \"Modifying\" if existing_entry else \"Adding\"\n        logger.info(\"%s stage '%s' in '%s'\", action, stage.name, self.relpath)\n\n        if existing_entry:\n            orig_stage_data = data[\"stages\"][stage.name]\n            if \"meta\" in orig_stage_data:\n                stage_data[stage.name][\"meta\"] = orig_stage_data[\"meta\"]\n            apply_diff(stage_data[stage.name], orig_stage_data)\n        else:\n            data[\"stages\"].update(stage_data)\n\n        dump_yaml(self.path, data)\n        self.repo.scm.track_file(self.relpath)\n\n    @property\n    def stage(self):\n        raise DvcException(\n            \"PipelineFile has multiple stages. Please specify it's name.\"\n        )\n\n    @property\n    def stages(self):\n        data, _ = self._load()\n        lockfile_data = self._lockfile.load()\n        return StageLoader(self, data.get(\"stages\", {}), lockfile_data)\n\n    def remove(self, force=False):\n        if not force:\n            logger.warning(\"Cannot remove pipeline file.\")\n            return\n\n        super().remove()\n        self._lockfile.remove()\n\n    def remove_stage(self, stage):\n        self._lockfile.remove_stage(stage)\n        if not self.exists():\n            return\n\n        with open(self.path, \"r\") as f:\n            d = parse_yaml_for_update(f.read(), self.path)\n\n        self.validate(d, self.path)\n        if stage.name not in d.get(\"stages\", {}):\n            return\n\n        logger.debug(\"Removing '%s' from '%s'\", stage.name, self.path)\n        del d[\"stages\"][stage.name]\n\n        if d[\"stages\"]:\n            dump_yaml(self.path, d)\n        else:\n            super().remove()\n\n\nclass Lockfile(FileMixin):\n    from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA\n\n    def load(self):\n        if not self.exists():\n            return {}\n        with self.repo.tree.open(self.path) as fd:\n            data = parse_yaml(fd.read(), self.path)\n        try:\n            self.validate(data, fname=self.relpath)\n        except StageFileFormatError:\n            raise LockfileCorruptedError(\n                f\"Lockfile '{self.relpath}' is corrupted.\"\n            )\n        return data\n\n    def dump(self, stage, **kwargs):\n        stage_data = serialize.to_lockfile(stage)\n        if not self.exists():\n            modified = True\n            logger.info(\"Generating lock file '%s'\", self.relpath)\n            data = stage_data\n            open(self.path, \"w+\").close()\n        else:\n            with self.repo.tree.open(self.path, \"r\") as fd:\n                data = parse_yaml_for_update(fd.read(), self.path)\n            modified = data.get(stage.name, {}) != stage_data.get(\n                stage.name, {}\n            )\n            if modified:\n                logger.info(\"Updating lock file '%s'\", self.relpath)\n            data.update(stage_data)\n        dump_yaml(self.path, data)\n        if modified:\n            self.repo.scm.track_file(self.relpath)\n\n    def remove_stage(self, stage):\n        if not self.exists():\n            return\n\n        with open(self.path) as f:\n            d = parse_yaml_for_update(f.read(), self.path)\n        self.validate(d, self.path)\n\n        if stage.name not in d:\n            return\n\n        logger.debug(\"Removing '%s' from '%s'\", stage.name, self.path)\n        del d[stage.name]\n\n        if d:\n            dump_yaml(self.path, d)\n        else:\n            self.remove()\n\n\nclass Dvcfile:\n    def __new__(cls, repo, path, **kwargs):\n        assert path\n        assert repo\n\n        _, ext = os.path.splitext(path)\n        if ext in [\".yaml\", \".yml\"]:\n            return PipelineFile(repo, path, **kwargs)\n        # fallback to single stage file for better error messages\n        return SingleStageFile(repo, path, **kwargs)\n", "path": "dvc/dvcfile.py"}]}