Coverage for python/lsst/daf/butler/_butlerConfig.py: 15%
65 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-23 09:30 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-23 09:30 +0000
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22"""
23Configuration classes specific to the Butler
24"""
25from __future__ import annotations
27__all__ = ("ButlerConfig",)
29import copy
30import os
31from collections.abc import Sequence
33from lsst.resources import ResourcePath, ResourcePathExpression
35from ._butlerRepoIndex import ButlerRepoIndex
36from .core import Config, DatastoreConfig, StorageClassConfig
37from .registry import RegistryConfig
38from .transfers import RepoTransferFormatConfig
40CONFIG_COMPONENT_CLASSES = (RegistryConfig, StorageClassConfig, DatastoreConfig, RepoTransferFormatConfig)
43class ButlerConfig(Config):
44 """Contains the configuration for a `Butler`
46 The configuration is read and merged with default configurations for
47 the particular classes. The defaults are read according to the rules
48 outlined in `ConfigSubset`. Each component of the configuration associated
49 with a configuration class reads its own defaults.
51 Parameters
52 ----------
53 other : `str`, `Config`, `ButlerConfig`, optional
54 Path to butler configuration YAML file or a directory containing a
55 "butler.yaml" file. If `None` the butler will
56 be configured based entirely on defaults read from the environment
57 or from ``searchPaths``.
58 No defaults will be read if a `ButlerConfig` is supplied directly.
59 searchPaths : `list` or `tuple`, optional
60 Explicit additional paths to search for defaults. They should
61 be supplied in priority order. These paths have higher priority
62 than those read from the environment in
63 `ConfigSubset.defaultSearchPaths()`. They are only read if ``other``
64 refers to a configuration file or directory.
65 """
67 def __init__(
68 self,
69 other: ResourcePathExpression | Config | None = None,
70 searchPaths: Sequence[ResourcePathExpression] | None = None,
71 ):
72 self.configDir: ResourcePath | None = None
74 # If this is already a ButlerConfig we assume that defaults
75 # have already been loaded.
76 if other is not None and isinstance(other, ButlerConfig):
77 super().__init__(other)
78 # Ensure that the configuration directory propagates
79 self.configDir = copy.copy(other.configDir)
80 return
82 # If a string is given it *could* be an alias that should be
83 # expanded by the repository index system.
84 original_other = other
85 resolved_alias = False
86 if isinstance(other, str):
87 try:
88 # Force back to a string because the resolved URI
89 # might not refer explicitly to a directory and we have
90 # check below to guess that.
91 other = str(ButlerRepoIndex.get_repo_uri(other, True))
92 except Exception:
93 pass
94 if other != original_other:
95 resolved_alias = True
97 # Include ResourcePath here in case it refers to a directory.
98 # Creating a ResourcePath from a ResourcePath is a no-op.
99 if isinstance(other, (str, os.PathLike, ResourcePath)):
100 # This will only allow supported schemes
101 uri = ResourcePath(other)
103 # We allow the butler configuration file to be left off the
104 # URI supplied by the user. If a directory-like URI is given
105 # we add the default configuration name.
107 # It's easy to miss a trailing / for remote URIs so try to guess
108 # we have been given a directory-like URI if there is no
109 # file extension. Local URIs do not need any guess work.
110 if not uri.isLocal and not uri.getExtension():
111 uri = ResourcePath(other, forceDirectory=True)
113 if uri.isdir():
114 # Could also be butler.json (for example in the butler
115 # server) but checking for existence will slow things
116 # down given that this might involve two checks and then
117 # the config read below would still do the read.
118 other = uri.join("butler.yaml")
120 # Create an empty config for us to populate
121 super().__init__()
123 # Read the supplied config so that we can work out which other
124 # defaults to use.
125 try:
126 butlerConfig = Config(other)
127 except FileNotFoundError as e:
128 # No reason to talk about aliases unless we were given a
129 # string and the alias was not resolved.
130 if isinstance(original_other, str):
131 if not resolved_alias:
132 # No alias was resolved. List known aliases if we have
133 # them or else explain a reason why aliasing might not
134 # have happened.
135 if known := ButlerRepoIndex.get_known_repos():
136 aliases = f"(given {original_other!r} and known aliases: {', '.join(known)})"
137 else:
138 failure_reason = ButlerRepoIndex.get_failure_reason()
139 if failure_reason:
140 failure_reason = f": {failure_reason}"
141 aliases = f"(given {original_other!r} and no known aliases{failure_reason})"
142 else:
143 aliases = f"(resolved from alias {original_other!r})"
144 errmsg = f"{e} {aliases}"
145 else:
146 errmsg = str(e)
147 raise FileNotFoundError(errmsg) from e
149 configFile = butlerConfig.configFile
150 if configFile is not None:
151 uri = ResourcePath(configFile)
152 self.configFile = uri
153 self.configDir = uri.dirname()
155 # A Butler config contains defaults defined by each of the component
156 # configuration classes. We ask each of them to apply defaults to
157 # the values we have been supplied by the user.
158 for configClass in CONFIG_COMPONENT_CLASSES:
159 # Only send the parent config if the child
160 # config component is present (otherwise it assumes that the
161 # keys from other components are part of the child)
162 localOverrides = None
163 if configClass.component in butlerConfig:
164 localOverrides = butlerConfig
165 config = configClass(localOverrides, searchPaths=searchPaths)
166 # Re-attach it using the global namespace
167 assert configClass.component is not None, "Config class component cannot be None"
168 self.update({configClass.component: config})
169 # Remove the key from the butlerConfig since we have already
170 # merged that information.
171 if configClass.component in butlerConfig:
172 del butlerConfig[configClass.component]
174 # Now that we have all the defaults we can merge the externally
175 # provided config into the defaults.
176 # Not needed if there is never information in a butler config file
177 # not present in component configurations
178 self.update(butlerConfig)