prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  dvc repro: Tuples in parameters are always marked as modified
  # 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
  
  <!--
  A clear and concise description of what the bug is.
  -->
  When tuples are used in parameters, dvc always marks them as modified.
  
  ```sh
  dvc status
  ```
  outputs
  ```yaml
  train_model:
    changed deps:
      model/parameters.py:    
        modified: NGRAM_VECTORIZER_RANGE
  ```
  
  regardless of whether `NGRAM_VECTORIZER_RANGE` has been changed
  
  
  --- 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/dependency/param.py`
  Content:
  ```
  1 import logging
  2 import os
  3 import typing
  4 from collections import defaultdict
  5 from typing import Dict, cast
  6 
  7 import dpath.util
  8 from voluptuous import Any
  9 
  10 from dvc.exceptions import DvcException
  11 from dvc.utils.serialize import ParseError, load_path
  12 from dvc_data.hashfile.hash_info import HashInfo
  13 
  14 from .base import Dependency
  15 
  16 logger = logging.getLogger(__name__)
  17 
  18 
  19 class MissingParamsError(DvcException):
  20     pass
  21 
  22 
  23 class MissingParamsFile(DvcException):
  24     pass
  25 
  26 
  27 class ParamsIsADirectoryError(DvcException):
  28     pass
  29 
  30 
  31 class BadParamFileError(DvcException):
  32     pass
  33 
  34 
  35 class ParamsDependency(Dependency):
  36     PARAM_PARAMS = "params"
  37     PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
  38     DEFAULT_PARAMS_FILE = "params.yaml"
  39 
  40     def __init__(self, stage, path, params=None, repo=None):
  41         self.params = list(params) if params else []
  42         hash_info = HashInfo()
  43         if isinstance(params, dict):
  44             hash_info = HashInfo(
  45                 self.PARAM_PARAMS,
  46                 params,  # type: ignore[arg-type]
  47             )
  48         repo = repo or stage.repo
  49         path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
  50         super().__init__(stage, path, repo=repo)
  51         self.hash_info = hash_info
  52 
  53     def dumpd(self, **kwargs):
  54         ret = super().dumpd()
  55         if not self.hash_info:
  56             ret[self.PARAM_PARAMS] = self.params or {}
  57         return ret
  58 
  59     def fill_values(self, values=None):
  60         """Load params values dynamically."""
  61         if values is None:
  62             return
  63 
  64         info = {}
  65         if not self.params:
  66             info.update(values)
  67         for param in self.params:
  68             if param in values:
  69                 info[param] = values[param]
  70         self.hash_info = HashInfo(
  71             self.PARAM_PARAMS,
  72             info,  # type: ignore[arg-type]
  73         )
  74 
  75     def read_params(
  76         self, flatten: bool = True, **kwargs: typing.Any
  77     ) -> Dict[str, typing.Any]:
  78         try:
  79             config = self.read_file()
  80         except MissingParamsFile:
  81             config = {}
  82 
  83         if not self.params:
  84             return config
  85 
  86         ret = {}
  87         if flatten:
  88             for param in self.params:
  89                 try:
  90                     ret[param] = dpath.util.get(config, param, separator=".")
  91                 except KeyError:
  92                     continue
  93             return ret
  94 
  95         from dpath.util import merge
  96 
  97         for param in self.params:
  98             merge(
  99                 ret,
  100                 dpath.util.search(config, param, separator="."),
  101                 separator=".",
  102             )
  103         return ret
  104 
  105     def workspace_status(self):
  106         if not self.exists:
  107             return {str(self): "deleted"}
  108         if self.hash_info.value is None:
  109             return {str(self): "new"}
  110 
  111         from funcy import ldistinct
  112 
  113         status: Dict[str, Any] = defaultdict(dict)
  114         info = cast(dict, self.hash_info.value) if self.hash_info else {}
  115         actual = self.read_params()
  116 
  117         # NOTE: we want to preserve the order of params as specified in the
  118         # status. In case of tracking the whole file, the order is top-level
  119         # keys in the file and then the keys in the `info` from `dvc.lock`
  120         # (which are alphabetically sorted).
  121         params = self.params or ldistinct([*actual.keys(), *info.keys()])
  122         for param in params:
  123             if param not in actual:
  124                 st = "deleted"
  125             elif param not in info:
  126                 st = "new"
  127             elif actual[param] != info[param]:
  128                 st = "modified"
  129             else:
  130                 assert actual[param] == info[param]
  131                 continue
  132 
  133             status[str(self)][param] = st
  134 
  135         return status
  136 
  137     def status(self):
  138         return self.workspace_status()
  139 
  140     def validate_filepath(self):
  141         if not self.exists:
  142             raise MissingParamsFile(f"Parameters file '{self}' does not exist")
  143         if self.isdir():
  144             raise ParamsIsADirectoryError(
  145                 f"'{self}' is a directory, expected a parameters file"
  146             )
  147 
  148     def read_file(self):
  149         self.validate_filepath()
  150         try:
  151             return load_path(self.fs_path, self.repo.fs)
  152         except ParseError as exc:
  153             raise BadParamFileError(
  154                 f"Unable to read parameters from '{self}'"
  155             ) from exc
  156 
  157     def get_hash(self):
  158         info = self.read_params()
  159 
  160         missing_params = set(self.params) - set(info.keys())
  161         if missing_params:
  162             raise MissingParamsError(
  163                 "Parameters '{}' are missing from '{}'.".format(
  164                     ", ".join(missing_params), self
  165                 )
  166             )
  167 
  168         return HashInfo(self.PARAM_PARAMS, info)  # type: ignore[arg-type]
  169 
  170     def save(self):
  171         if not self.exists:
  172             raise self.DoesNotExistError(self)
  173 
  174         if not self.isfile() and not self.isdir():
  175             raise self.IsNotFileOrDirError(self)
  176 
  177         self.ignore()
  178         self.hash_info = self.get_hash()
  179
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/dvc/dependency/param.py b/dvc/dependency/param.py
  --- a/dvc/dependency/param.py
  +++ b/dvc/dependency/param.py
  @@ -125,9 +125,14 @@
               elif param not in info:
                   st = "new"
               elif actual[param] != info[param]:
  -                st = "modified"
  +                if (
  +                    isinstance(actual[param], tuple)
  +                    and list(actual[param]) == info[param]
  +                ):
  +                    continue
  +                else:
  +                    st = "modified"
               else:
  -                assert actual[param] == info[param]
                   continue
   
               status[str(self)][param] = st
  
verification_info: {"golden_diff": "diff --git a/dvc/dependency/param.py b/dvc/dependency/param.py\n--- a/dvc/dependency/param.py\n+++ b/dvc/dependency/param.py\n@@ -125,9 +125,14 @@\n             elif param not in info:\n                 st = \"new\"\n             elif actual[param] != info[param]:\n-                st = \"modified\"\n+                if (\n+                    isinstance(actual[param], tuple)\n+                    and list(actual[param]) == info[param]\n+                ):\n+                    continue\n+                else:\n+                    st = \"modified\"\n             else:\n-                assert actual[param] == info[param]\n                 continue\n \n             status[str(self)][param] = st\n", "issue": "dvc repro: Tuples in parameters are always marked as modified\n# Bug Report\r\n\r\n<!--\r\n## Issue name\r\n\r\nIssue 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.\r\n\r\nExample: `repro: doesn't detect input changes`\r\n-->\r\n\r\n## Description\r\n\r\n<!--\r\nA clear and concise description of what the bug is.\r\n-->\r\nWhen tuples are used in parameters, dvc always marks them as modified.\r\n\r\n```sh\r\ndvc status\r\n```\r\noutputs\r\n```yaml\r\ntrain_model:\r\n  changed deps:\r\n    model/parameters.py:    \r\n      modified: NGRAM_VECTORIZER_RANGE\r\n```\r\n\r\nregardless of whether `NGRAM_VECTORIZER_RANGE` has been changed\r\n\n", "before_files": [{"content": "import logging\nimport os\nimport typing\nfrom collections import defaultdict\nfrom typing import Dict, cast\n\nimport dpath.util\nfrom voluptuous import Any\n\nfrom dvc.exceptions import DvcException\nfrom dvc.utils.serialize import ParseError, load_path\nfrom dvc_data.hashfile.hash_info import HashInfo\n\nfrom .base import Dependency\n\nlogger = logging.getLogger(__name__)\n\n\nclass MissingParamsError(DvcException):\n    pass\n\n\nclass MissingParamsFile(DvcException):\n    pass\n\n\nclass ParamsIsADirectoryError(DvcException):\n    pass\n\n\nclass BadParamFileError(DvcException):\n    pass\n\n\nclass ParamsDependency(Dependency):\n    PARAM_PARAMS = \"params\"\n    PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}\n    DEFAULT_PARAMS_FILE = \"params.yaml\"\n\n    def __init__(self, stage, path, params=None, repo=None):\n        self.params = list(params) if params else []\n        hash_info = HashInfo()\n        if isinstance(params, dict):\n            hash_info = HashInfo(\n                self.PARAM_PARAMS,\n                params,  # type: ignore[arg-type]\n            )\n        repo = repo or stage.repo\n        path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)\n        super().__init__(stage, path, repo=repo)\n        self.hash_info = hash_info\n\n    def dumpd(self, **kwargs):\n        ret = super().dumpd()\n        if not self.hash_info:\n            ret[self.PARAM_PARAMS] = self.params or {}\n        return ret\n\n    def fill_values(self, values=None):\n        \"\"\"Load params values dynamically.\"\"\"\n        if values is None:\n            return\n\n        info = {}\n        if not self.params:\n            info.update(values)\n        for param in self.params:\n            if param in values:\n                info[param] = values[param]\n        self.hash_info = HashInfo(\n            self.PARAM_PARAMS,\n            info,  # type: ignore[arg-type]\n        )\n\n    def read_params(\n        self, flatten: bool = True, **kwargs: typing.Any\n    ) -> Dict[str, typing.Any]:\n        try:\n            config = self.read_file()\n        except MissingParamsFile:\n            config = {}\n\n        if not self.params:\n            return config\n\n        ret = {}\n        if flatten:\n            for param in self.params:\n                try:\n                    ret[param] = dpath.util.get(config, param, separator=\".\")\n                except KeyError:\n                    continue\n            return ret\n\n        from dpath.util import merge\n\n        for param in self.params:\n            merge(\n                ret,\n                dpath.util.search(config, param, separator=\".\"),\n                separator=\".\",\n            )\n        return ret\n\n    def workspace_status(self):\n        if not self.exists:\n            return {str(self): \"deleted\"}\n        if self.hash_info.value is None:\n            return {str(self): \"new\"}\n\n        from funcy import ldistinct\n\n        status: Dict[str, Any] = defaultdict(dict)\n        info = cast(dict, self.hash_info.value) if self.hash_info else {}\n        actual = self.read_params()\n\n        # NOTE: we want to preserve the order of params as specified in the\n        # status. In case of tracking the whole file, the order is top-level\n        # keys in the file and then the keys in the `info` from `dvc.lock`\n        # (which are alphabetically sorted).\n        params = self.params or ldistinct([*actual.keys(), *info.keys()])\n        for param in params:\n            if param not in actual:\n                st = \"deleted\"\n            elif param not in info:\n                st = \"new\"\n            elif actual[param] != info[param]:\n                st = \"modified\"\n            else:\n                assert actual[param] == info[param]\n                continue\n\n            status[str(self)][param] = st\n\n        return status\n\n    def status(self):\n        return self.workspace_status()\n\n    def validate_filepath(self):\n        if not self.exists:\n            raise MissingParamsFile(f\"Parameters file '{self}' does not exist\")\n        if self.isdir():\n            raise ParamsIsADirectoryError(\n                f\"'{self}' is a directory, expected a parameters file\"\n            )\n\n    def read_file(self):\n        self.validate_filepath()\n        try:\n            return load_path(self.fs_path, self.repo.fs)\n        except ParseError as exc:\n            raise BadParamFileError(\n                f\"Unable to read parameters from '{self}'\"\n            ) from exc\n\n    def get_hash(self):\n        info = self.read_params()\n\n        missing_params = set(self.params) - set(info.keys())\n        if missing_params:\n            raise MissingParamsError(\n                \"Parameters '{}' are missing from '{}'.\".format(\n                    \", \".join(missing_params), self\n                )\n            )\n\n        return HashInfo(self.PARAM_PARAMS, info)  # type: ignore[arg-type]\n\n    def save(self):\n        if not self.exists:\n            raise self.DoesNotExistError(self)\n\n        if not self.isfile() and not self.isdir():\n            raise self.IsNotFileOrDirError(self)\n\n        self.ignore()\n        self.hash_info = self.get_hash()\n", "path": "dvc/dependency/param.py"}], "after_files": [{"content": "import logging\nimport os\nimport typing\nfrom collections import defaultdict\nfrom typing import Dict, cast\n\nimport dpath.util\nfrom voluptuous import Any\n\nfrom dvc.exceptions import DvcException\nfrom dvc.utils.serialize import ParseError, load_path\nfrom dvc_data.hashfile.hash_info import HashInfo\n\nfrom .base import Dependency\n\nlogger = logging.getLogger(__name__)\n\n\nclass MissingParamsError(DvcException):\n    pass\n\n\nclass MissingParamsFile(DvcException):\n    pass\n\n\nclass ParamsIsADirectoryError(DvcException):\n    pass\n\n\nclass BadParamFileError(DvcException):\n    pass\n\n\nclass ParamsDependency(Dependency):\n    PARAM_PARAMS = \"params\"\n    PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}\n    DEFAULT_PARAMS_FILE = \"params.yaml\"\n\n    def __init__(self, stage, path, params=None, repo=None):\n        self.params = list(params) if params else []\n        hash_info = HashInfo()\n        if isinstance(params, dict):\n            hash_info = HashInfo(\n                self.PARAM_PARAMS,\n                params,  # type: ignore[arg-type]\n            )\n        repo = repo or stage.repo\n        path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)\n        super().__init__(stage, path, repo=repo)\n        self.hash_info = hash_info\n\n    def dumpd(self, **kwargs):\n        ret = super().dumpd()\n        if not self.hash_info:\n            ret[self.PARAM_PARAMS] = self.params or {}\n        return ret\n\n    def fill_values(self, values=None):\n        \"\"\"Load params values dynamically.\"\"\"\n        if values is None:\n            return\n\n        info = {}\n        if not self.params:\n            info.update(values)\n        for param in self.params:\n            if param in values:\n                info[param] = values[param]\n        self.hash_info = HashInfo(\n            self.PARAM_PARAMS,\n            info,  # type: ignore[arg-type]\n        )\n\n    def read_params(\n        self, flatten: bool = True, **kwargs: typing.Any\n    ) -> Dict[str, typing.Any]:\n        try:\n            config = self.read_file()\n        except MissingParamsFile:\n            config = {}\n\n        if not self.params:\n            return config\n\n        ret = {}\n        if flatten:\n            for param in self.params:\n                try:\n                    ret[param] = dpath.util.get(config, param, separator=\".\")\n                except KeyError:\n                    continue\n            return ret\n\n        from dpath.util import merge\n\n        for param in self.params:\n            merge(\n                ret,\n                dpath.util.search(config, param, separator=\".\"),\n                separator=\".\",\n            )\n        return ret\n\n    def workspace_status(self):\n        if not self.exists:\n            return {str(self): \"deleted\"}\n        if self.hash_info.value is None:\n            return {str(self): \"new\"}\n\n        from funcy import ldistinct\n\n        status: Dict[str, Any] = defaultdict(dict)\n        info = cast(dict, self.hash_info.value) if self.hash_info else {}\n        actual = self.read_params()\n\n        # NOTE: we want to preserve the order of params as specified in the\n        # status. In case of tracking the whole file, the order is top-level\n        # keys in the file and then the keys in the `info` from `dvc.lock`\n        # (which are alphabetically sorted).\n        params = self.params or ldistinct([*actual.keys(), *info.keys()])\n        for param in params:\n            if param not in actual:\n                st = \"deleted\"\n            elif param not in info:\n                st = \"new\"\n            elif actual[param] != info[param]:\n                if (\n                    isinstance(actual[param], tuple)\n                    and list(actual[param]) == info[param]\n                ):\n                    continue\n                else:\n                    st = \"modified\"\n            else:\n                continue\n\n            status[str(self)][param] = st\n\n        return status\n\n    def status(self):\n        return self.workspace_status()\n\n    def validate_filepath(self):\n        if not self.exists:\n            raise MissingParamsFile(f\"Parameters file '{self}' does not exist\")\n        if self.isdir():\n            raise ParamsIsADirectoryError(\n                f\"'{self}' is a directory, expected a parameters file\"\n            )\n\n    def read_file(self):\n        self.validate_filepath()\n        try:\n            return load_path(self.fs_path, self.repo.fs)\n        except ParseError as exc:\n            raise BadParamFileError(\n                f\"Unable to read parameters from '{self}'\"\n            ) from exc\n\n    def get_hash(self):\n        info = self.read_params()\n\n        missing_params = set(self.params) - set(info.keys())\n        if missing_params:\n            raise MissingParamsError(\n                \"Parameters '{}' are missing from '{}'.\".format(\n                    \", \".join(missing_params), self\n                )\n            )\n\n        return HashInfo(self.PARAM_PARAMS, info)  # type: ignore[arg-type]\n\n    def save(self):\n        if not self.exists:\n            raise self.DoesNotExistError(self)\n\n        if not self.isfile() and not self.isdir():\n            raise self.IsNotFileOrDirError(self)\n\n        self.ignore()\n        self.hash_info = self.get_hash()\n", "path": "dvc/dependency/param.py"}]}