1import os
2import warnings
3
4import toml
5from more_itertools import always_iterable
6
7from yt._maintenance.deprecation import issue_deprecation_warning
8from yt.utilities.configuration_tree import ConfigLeaf, ConfigNode
9
10ytcfg_defaults = {}
11
12ytcfg_defaults["yt"] = dict(
13    serialize=False,
14    only_deserialize=False,
15    time_functions=False,
16    colored_logs=False,
17    suppress_stream_logging=False,
18    stdout_stream_logging=False,
19    log_level=20,
20    inline=False,
21    num_threads=-1,
22    store_parameter_files=False,
23    parameter_file_store="parameter_files.csv",
24    maximum_stored_datasets=500,
25    skip_dataset_cache=True,
26    load_field_plugins=False,
27    plugin_filename="my_plugins.py",
28    parallel_traceback=False,
29    pasteboard_repo="",
30    reconstruct_index=True,
31    test_storage_dir="/does/not/exist",
32    test_data_dir="/does/not/exist",
33    enzo_db="",
34    notebook_password="",
35    answer_testing_tolerance=3,
36    answer_testing_bitwise=False,
37    gold_standard_filename="gold311",
38    local_standard_filename="local001",
39    answer_tests_url="http://answers.yt-project.org/{1}_{2}",
40    sketchfab_api_key="None",
41    imagebin_api_key="e1977d9195fe39e",
42    imagebin_upload_url="https://api.imgur.com/3/image",
43    imagebin_delete_url="https://api.imgur.com/3/image/{delete_hash}",
44    curldrop_upload_url="http://use.yt/upload",
45    thread_field_detection=False,
46    ignore_invalid_unit_operation_errors=False,
47    chunk_size=1000,
48    xray_data_dir="/does/not/exist",
49    supp_data_dir="/does/not/exist",
50    default_colormap="arbre",
51    ray_tracing_engine="embree",
52    internals=dict(
53        within_testing=False,
54        within_pytest=False,
55        parallel=False,
56        strict_requires=False,
57        global_parallel_rank=0,
58        global_parallel_size=1,
59        topcomm_parallel_rank=0,
60        topcomm_parallel_size=1,
61        command_line=False,
62    ),
63)
64
65
66def config_dir():
67    config_root = os.environ.get(
68        "XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")
69    )
70    conf_dir = os.path.join(config_root, "yt")
71
72    if not os.path.exists(conf_dir):
73        try:
74            os.makedirs(conf_dir)
75        except OSError:
76            warnings.warn("unable to create yt config directory")
77    return conf_dir
78
79
80def old_config_file():
81    return os.path.join(config_dir(), "ytrc")
82
83
84def old_config_dir():
85    return os.path.join(os.path.expanduser("~"), ".yt")
86
87
88# For backward compatibility, do not use these vars internally in yt
89CONFIG_DIR = config_dir()
90_OLD_CONFIG_FILE = old_config_file()
91
92
93class YTConfig:
94    def __init__(self, defaults=None):
95        if defaults is None:
96            defaults = {}
97        self.config_root = ConfigNode(None)
98
99    def get(self, section, *keys, callback=None):
100        node_or_leaf = self.config_root.get(section, *keys)
101        if isinstance(node_or_leaf, ConfigLeaf):
102            if callback is not None:
103                return callback(node_or_leaf)
104            return node_or_leaf.value
105        return node_or_leaf
106
107    def get_most_specific(self, section, *keys, **kwargs):
108        use_fallback = "fallback" in kwargs
109        fallback = kwargs.pop("fallback", None)
110        try:
111            return self.config_root.get_deepest_leaf(section, *keys)
112        except KeyError as err:
113            if use_fallback:
114                return fallback
115            else:
116                raise err
117
118    def update(self, new_values, metadata=None):
119        if metadata is None:
120            metadata = {}
121        self.config_root.update(new_values, metadata)
122
123    def has_section(self, section):
124        try:
125            self.config_root.get_child(section)
126            return True
127        except KeyError:
128            return False
129
130    def add_section(self, section):
131        self.config_root.add_child(section)
132
133    def remove_section(self, section):
134        if self.has_section(section):
135            self.config_root.remove_child(section)
136            return True
137        else:
138            return False
139
140    def set(self, *args, metadata=None):
141        section, *keys, value = args
142        if metadata is None:
143            metadata = {"source": "runtime"}
144        self.config_root.upsert_from_list(
145            [section] + list(keys), value, extra_data=metadata
146        )
147
148    def remove(self, *args):
149        self.config_root.pop_leaf(args)
150
151    def read(self, file_names):
152        file_names_read = []
153        for fname in always_iterable(file_names):
154            if not os.path.exists(fname):
155                continue
156            metadata = {"source": f"file: {fname}"}
157            self.update(toml.load(fname), metadata=metadata)
158            file_names_read.append(fname)
159
160        return file_names_read
161
162    def write(self, file_handler):
163        value = self.config_root.as_dict()
164        config_as_str = toml.dumps(value)
165
166        try:
167            # Assuming file_handler has a write attribute
168            file_handler.write(config_as_str)
169        except AttributeError:
170            # Otherwise we expect a path to a file
171            with open(file_handler, mode="w") as fh:
172                fh.write(config_as_str)
173
174    @staticmethod
175    def get_global_config_file():
176        return os.path.join(config_dir(), "yt.toml")
177
178    @staticmethod
179    def get_local_config_file():
180        return os.path.join(os.path.abspath(os.curdir), "yt.toml")
181
182    def __setitem__(self, args, value):
183        section, *keys = always_iterable(args)
184        self.set(section, *keys, value, metadata=None)
185
186    def __getitem__(self, key):
187        section, *keys = always_iterable(key)
188        return self.get(section, *keys)
189
190    def __contains__(self, item):
191        return item in self.config_root
192
193    # Add support for IPython rich display
194    # see https://ipython.readthedocs.io/en/stable/config/integrating.html
195    def _repr_json_(self):
196        return self.config_root._repr_json_()
197
198
199_global_config_file = YTConfig.get_global_config_file()
200_local_config_file = YTConfig.get_local_config_file()
201
202if os.path.exists(old_config_file()):
203    if os.path.exists(_global_config_file):
204        issue_deprecation_warning(
205            f"The configuration file {old_config_file()} is deprecated in "
206            f"favor of {_global_config_file}. Currently, both are present. "
207            "Please manually remove the deprecated one to silence "
208            "this warning.",
209            since="4.0.0",
210            removal="4.1.0",
211        )
212    else:
213        issue_deprecation_warning(
214            f"The configuration file {_OLD_CONFIG_FILE} is deprecated. "
215            f"Please migrate your config to {_global_config_file} by running: "
216            "'yt config migrate'",
217            since="4.0.0",
218            removal="4.1.0",
219        )
220
221
222if not os.path.exists(_global_config_file):
223    cfg = {"yt": {}}
224    try:
225        with open(_global_config_file, mode="w") as fd:
226            toml.dump(cfg, fd)
227    except OSError:
228        warnings.warn("unable to write new config file")
229
230
231# Load the config
232ytcfg = YTConfig()
233ytcfg.update(ytcfg_defaults, metadata={"source": "defaults"})
234
235# Try loading the local config first, otherwise fall back to global config
236if os.path.exists(_local_config_file):
237    ytcfg.read(_local_config_file)
238elif os.path.exists(_global_config_file):
239    ytcfg.read(_global_config_file)
240