Repository: iterative/dvc

Issue:
repro: a Greek letter in parameters name cause an error
# Bug Report

<!--
## Issue name

Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug. 

Example: `repro: doesn't detect input changes`
-->

## Description

`dvc repro --dry` throws the error
```
ERROR: failed to reproduce 'dvc.yaml': Parameters 'background_fits.mÎ©b_bgd_range' are missing from 'settings.json'.
```
clearly misreplacing the greek symbol `Ω`

The name of the parameter is correct in dvc.yaml
```
    - settings.json:
      - background_fits.mΩb_bgd_range
```

It is correct in settings.json:
```
       "background_fits": {
                              "mΩb_bgd_range": [
                                                 6.1,
                                                 6.4
                                               ]
                          },
```
Also fine in dvc.lock
```
      background_fits.mΩb_bgd_range:
      - 6.1
      - 6.4
```

### Environment information

<!--
This is required to ensure that we can reproduce the bug.
-->

**Output of `dvc version`:**

```console
$ dvc version
```

```
λ dvc version
DVC version: 1.11.10 (exe)
---------------------------------
Platform: Python 3.7.9 on Windows-10-10.0.19041-SP0
Supports: All remotes
Cache types: hardlink
Cache directory: NTFS on C:\
Caches: local
Remotes: None
Workspace directory: NTFS on C:\
Repo: dvc, git
```



Discussion:

Resolving patch:
diff --git a/dvc/dvcfile.py b/dvc/dvcfile.py
--- a/dvc/dvcfile.py
+++ b/dvc/dvcfile.py
@@ -131,7 +131,7 @@ def _load(self):
             raise StageFileIsNotDvcFileError(self.path)
         if self._is_git_ignored():
             raise FileIsGitIgnored(self.path)
-        with self.repo.tree.open(self.path) as fd:
+        with self.repo.tree.open(self.path, encoding="utf-8") as fd:
             stage_text = fd.read()
         d = parse_yaml(stage_text, self.path)
         return self.validate(d, self.relpath), stage_text
@@ -276,7 +276,7 @@ def remove_stage(self, stage):
         if not self.exists():
             return
 
-        with open(self.path, "r") as f:
+        with open(self.path, "r", encoding="utf-8") as f:
             d = parse_yaml_for_update(f.read(), self.path)
 
         self.validate(d, self.path)
@@ -380,7 +380,7 @@ def remove_stage(self, stage):
         if not self.exists():
             return
 
-        with open(self.path) as f:
+        with open(self.path, encoding="utf-8") as f:
             d = parse_yaml_for_update(f.read(), self.path)
         self.validate(d, self.path)
 


Test patch:
diff --git a/tests/unit/test_params.py b/tests/unit/test_params.py
--- a/tests/unit/test_params.py
+++ b/tests/unit/test_params.py
@@ -20,3 +20,15 @@ def test_params_order(tmp_dir, dvc, dummy_stage):
 
     # params are sorted during dumping, therefore p1 is first
     assert list(dvc.params.show()[""]) == ["p1.yaml", p2_path, "params.yaml"]
+
+
+def test_repro_unicode(tmp_dir, dvc, dummy_stage):
+    tmp_dir.gen({"settings.json": '{"Ω_value": 1}'})
+    dummy_stage(params=[{"settings.json": ["Ω_value"]}])
+    (stage,) = dvc.reproduce(dry=True)
+    stage.cmd = "foo"
+    stage.dvcfile._lockfile.dump(stage)
+
+    dvc.remove(stage.name)
+    assert not (tmp_dir / "dvc.yaml").exists()
+    assert not (tmp_dir / "dvc.lock").exists()