edq.config.source

  1import os
  2import typing
  3
  4import edq.config.common
  5import edq.util.serial
  6
  7class ConfigSourceSpec(edq.config.common.InternalConfigSourceSpec):
  8    """
  9    A source spec represents a location where config values may be stored.
 10    """
 11
 12class CLIExplicitSpec(ConfigSourceSpec):
 13    """
 14    A source spec for loading from CLI arguments (but not files specified on the command-line).
 15    Explicit refers to arguments that are explicitly noted with the `--config` flag.
 16    """
 17
 18    label: str = 'cli argument (explicit)'
 19
 20    def get_help_lines(self) -> typing.List[str]:
 21        return [
 22            'Command-Line Arguments (Explicit)',
 23            'Load configuration values set directly on the command-line via the `--config` flag.',
 24            '"Explicit" refers to the fact that these arguments use the `--config` flag.',
 25            'Multiple instances may be supplied, and they will be processed in the order provided.',
 26        ]
 27
 28class CLIImplicitSpec(ConfigSourceSpec):
 29    """
 30    A source spec for loading from CLI arguments (but not files specified on the command-line).
 31    Implicit refers to arguments that are not explicitly supplied with the `--config` flag,
 32    and instead specified with other flags.
 33    For example, a CLI may accept a `--server` flag that sets a `server` argument.
 34    """
 35
 36
 37    label: str = 'cli argument (implicit)'
 38
 39    def get_help_lines(self) -> typing.List[str]:
 40        return [
 41            'Command-Line Arguments (Implicit)',
 42            'Load configuration values set directly on the command-line.',
 43            '"Implicit" refers to the fact that these arguments do not use the `--config` flag.',
 44            'Multiple instances may be supplied, and they will be processed in the order provided.',
 45            'Default values from implicit command-line arguments will not override previously set values.',
 46        ]
 47
 48class CLIFileSpec(ConfigSourceSpec):
 49    """ A source spec for loading a file that was specified as a CLI argument. """
 50
 51    label: str = 'cli config file'
 52
 53    def get_help_lines(self) -> typing.List[str]:
 54        return [
 55            'Command-Line Files',
 56            'Load configuration files specified on the command-line via the `--config-file` flag.',
 57            'Multiple instances may be supplied, and they will be processed in the order provided.',
 58        ]
 59
 60class ENVSpec(ConfigSourceSpec):
 61    """ A source spec for loading from an environmental variable. """
 62
 63    label: str = 'environmental variable'
 64
 65    def get_help_lines(self) -> typing.List[str]:
 66        return [
 67            'Environmental Variables',
 68            'Load configuration values specified in environmental variables.',
 69            f"Variable names must start with the prefix: '{edq.config.common._env_prefix}'.",
 70            'Variable names will be stripped of the above prefix and turned to lower case.',
 71        ]
 72
 73class AbstractPathSpec(ConfigSourceSpec):
 74    """
 75    An abstract source spec that includes a target directory and filename.
 76
 77    An argument not set (or set to None) indicates that the value should be determined at parse time.
 78    These values (generally paths or filenames) are often pulled from the `edq.config.settings` module
 79    (and therefore can be set by users of this library anytime before loading the config).
 80    """
 81
 82    def __init__(self,
 83            base_dir: typing.Union[str, None] = None,
 84            filename: typing.Union[str, None] = None,
 85            **kwargs: typing.Any) -> None:
 86        super().__init__()
 87
 88        if ((base_dir is not None) and (os.path.isfile(base_dir))):
 89            raise ValueError(f"The specified config dir is not a directory: '{base_dir}'.")
 90
 91        self.base_dir: typing.Union[str, None] = base_dir
 92        """ The directory to look for the global config file in. """
 93
 94        self.filename: typing.Union[str, None] = filename
 95        """ The filename to look for. """
 96
 97    def get_base_dir(self) -> typing.Union[str, None]:
 98        """ Get the base dir for this spec. """
 99
100        if (self.base_dir is not None):
101            return self.base_dir
102
103        return self.get_default_base_dir()
104
105    def get_filename(self) -> typing.Union[str, None]:
106        """ Get the filename for this spec. """
107
108        if (self.filename is not None):
109            return self.filename
110
111        return self.get_default_filename()
112
113    def get_default_base_dir(self) -> typing.Union[str, None]:
114        """ Retrieve the default base dir (usually from edq.config.settings) for use when no base dir is explicitly set. """
115
116        return None
117
118    def get_default_filename(self) -> typing.Union[str, None]:
119        """ Retrieve the default filename (usually from edq.config.settings) for use when no filename is explicitly set. """
120
121        return None
122
123    def resolve_path(self,
124            override_path: typing.Union[str, None] = None,
125            ) -> str:
126        """
127        Get the path that should be use to load config.
128        This method is free to traverse the disk to find the desired path.
129        """
130
131        if (override_path is not None):
132            return override_path
133
134        base_dir = self.get_base_dir()
135        if (base_dir is None):
136            raise ValueError(f"Unable to resolve a config base dir for class {type(self)}.")
137
138        filename = self.get_filename()
139        if (filename is None):
140            raise ValueError(f"Unable to resolve a config filename for class {type(self)}.")
141
142        return os.path.join(base_dir, filename)
143
144class GlobalSpec(AbstractPathSpec):
145    """ A source spec for loading from a global (user-level) config file. """
146
147    label: str = 'global config file'
148
149    def get_help_lines(self) -> typing.List[str]:
150        return [
151            'Global Config File',
152            'Load configurations from a "global" config file (e.g., a file that stays the same for a specific user).',
153            'This file is a good location for configuration values that do not change from project to project,',
154            'such as usernames and passwords/tokens.',
155            f"For this machine and user, the global config is located at: '{self.resolve_path()}'.",
156        ]
157
158    def get_default_base_dir(self) -> typing.Union[str, None]:
159        return edq.config.common._global_dir
160
161    def get_default_filename(self) -> typing.Union[str, None]:
162        return edq.config.common._config_filename
163
164class LocalSpec(AbstractPathSpec):
165    """ A source spec for loading from a local config file. """
166
167    label: str = 'local config file'
168
169    def get_help_lines(self) -> typing.List[str]:
170        return [
171            'Local Config File',
172            'Load configurations from a "local" config file inside of the current directory.',
173            f"The file must be named: '{self.get_filename()}'.",
174        ]
175
176    def get_default_base_dir(self) -> typing.Union[str, None]:
177        return os.path.abspath(os.getcwd())
178
179    def get_default_filename(self) -> typing.Union[str, None]:
180        return edq.config.common._config_filename
181
182class ProjectSpec(AbstractPathSpec):
183    """
184    A source spec for loading from a project, which will look in the current directory and all parents until a matching file is found.
185    """
186
187    label: str = 'project config file'
188
189    def get_help_lines(self) -> typing.List[str]:
190        return [
191            'Project Config File',
192            'Load configurations from a "project" config file.',
193            'To find a project configuration file, we will look in the current directory and all parents (until root).',
194            'The first file found with the correct name will be loaded.',
195            'It is recommended to keep this file in your project\'s root directory.',
196            'This allows you to execute commands from anywhere in the project using the correct configuration settings.',
197            f"The file must be named: '{self.get_filename()}'.",
198        ]
199
200    def __init__(self,
201            root_cutoff: typing.Union[str, None] = None,
202            filename: typing.Union[str, None] = None,
203            **kwargs: typing.Any) -> None:
204        super().__init__(**kwargs)
205
206        self.root_cutoff: typing.Union[str, None] = root_cutoff
207        """ If set, stop searching parents if this directory is encountered. """
208
209    def get_default_base_dir(self) -> typing.Union[str, None]:
210        return os.path.abspath(os.getcwd())
211
212    def get_default_filename(self) -> typing.Union[str, None]:
213        return edq.config.common._config_filename
214
215    def resolve_path(self,
216            override_path: typing.Union[str, None] = None,
217            ) -> str:
218        if (override_path is not None):
219            return override_path
220
221        base_dir = self.get_base_dir()
222        if (base_dir is None):
223            raise ValueError(f"Unable to resolve a config base dir for class {type(self)}.")
224
225        filename = self.get_filename()
226        if (filename is None):
227            raise ValueError(f"Unable to resolve a config filename for class {type(self)}.")
228
229        current_dir = base_dir
230        for _ in range(edq.util.dirent.DEPTH_LIMIT):
231            path = os.path.join(current_dir, filename)
232            if (os.path.isfile(path)):
233                return path
234
235            # Check if current directory is root.
236            parent_dir = os.path.dirname(current_dir)
237            if (parent_dir == current_dir):
238                break
239
240            if ((self.root_cutoff is not None) and (self.root_cutoff == current_dir)):
241                break
242
243            current_dir = parent_dir
244
245        # If no file was found, use the base dir.
246        return os.path.join(base_dir, filename)
247
248edq.config.common._DEFAULT_LOAD_ORDER = [
249    GlobalSpec(),
250    ProjectSpec(),
251    ENVSpec(),
252    CLIFileSpec(),
253    CLIImplicitSpec(),
254    CLIExplicitSpec(),
255]
256edq.config.common._load_order = edq.config.common._DEFAULT_LOAD_ORDER.copy()
class ConfigSourceSpec(edq.config.common.InternalConfigSourceSpec):
 8class ConfigSourceSpec(edq.config.common.InternalConfigSourceSpec):
 9    """
10    A source spec represents a location where config values may be stored.
11    """

A source spec represents a location where config values may be stored.

class CLIExplicitSpec(ConfigSourceSpec):
13class CLIExplicitSpec(ConfigSourceSpec):
14    """
15    A source spec for loading from CLI arguments (but not files specified on the command-line).
16    Explicit refers to arguments that are explicitly noted with the `--config` flag.
17    """
18
19    label: str = 'cli argument (explicit)'
20
21    def get_help_lines(self) -> typing.List[str]:
22        return [
23            'Command-Line Arguments (Explicit)',
24            'Load configuration values set directly on the command-line via the `--config` flag.',
25            '"Explicit" refers to the fact that these arguments use the `--config` flag.',
26            'Multiple instances may be supplied, and they will be processed in the order provided.',
27        ]

A source spec for loading from CLI arguments (but not files specified on the command-line). Explicit refers to arguments that are explicitly noted with the --config flag.

label: str = 'cli argument (explicit)'

A label set by each subclass to identify the type of source spec. Generally, a user-friendly version of the class name.

def get_help_lines(self) -> List[str]:
21    def get_help_lines(self) -> typing.List[str]:
22        return [
23            'Command-Line Arguments (Explicit)',
24            'Load configuration values set directly on the command-line via the `--config` flag.',
25            '"Explicit" refers to the fact that these arguments use the `--config` flag.',
26            'Multiple instances may be supplied, and they will be processed in the order provided.',
27        ]

Get lines of text describing how this source is loaded. This text is intended to be used in a help prompt.

class CLIImplicitSpec(ConfigSourceSpec):
29class CLIImplicitSpec(ConfigSourceSpec):
30    """
31    A source spec for loading from CLI arguments (but not files specified on the command-line).
32    Implicit refers to arguments that are not explicitly supplied with the `--config` flag,
33    and instead specified with other flags.
34    For example, a CLI may accept a `--server` flag that sets a `server` argument.
35    """
36
37
38    label: str = 'cli argument (implicit)'
39
40    def get_help_lines(self) -> typing.List[str]:
41        return [
42            'Command-Line Arguments (Implicit)',
43            'Load configuration values set directly on the command-line.',
44            '"Implicit" refers to the fact that these arguments do not use the `--config` flag.',
45            'Multiple instances may be supplied, and they will be processed in the order provided.',
46            'Default values from implicit command-line arguments will not override previously set values.',
47        ]

A source spec for loading from CLI arguments (but not files specified on the command-line). Implicit refers to arguments that are not explicitly supplied with the --config flag, and instead specified with other flags. For example, a CLI may accept a --server flag that sets a server argument.

label: str = 'cli argument (implicit)'

A label set by each subclass to identify the type of source spec. Generally, a user-friendly version of the class name.

def get_help_lines(self) -> List[str]:
40    def get_help_lines(self) -> typing.List[str]:
41        return [
42            'Command-Line Arguments (Implicit)',
43            'Load configuration values set directly on the command-line.',
44            '"Implicit" refers to the fact that these arguments do not use the `--config` flag.',
45            'Multiple instances may be supplied, and they will be processed in the order provided.',
46            'Default values from implicit command-line arguments will not override previously set values.',
47        ]

Get lines of text describing how this source is loaded. This text is intended to be used in a help prompt.

class CLIFileSpec(ConfigSourceSpec):
49class CLIFileSpec(ConfigSourceSpec):
50    """ A source spec for loading a file that was specified as a CLI argument. """
51
52    label: str = 'cli config file'
53
54    def get_help_lines(self) -> typing.List[str]:
55        return [
56            'Command-Line Files',
57            'Load configuration files specified on the command-line via the `--config-file` flag.',
58            'Multiple instances may be supplied, and they will be processed in the order provided.',
59        ]

A source spec for loading a file that was specified as a CLI argument.

label: str = 'cli config file'

A label set by each subclass to identify the type of source spec. Generally, a user-friendly version of the class name.

def get_help_lines(self) -> List[str]:
54    def get_help_lines(self) -> typing.List[str]:
55        return [
56            'Command-Line Files',
57            'Load configuration files specified on the command-line via the `--config-file` flag.',
58            'Multiple instances may be supplied, and they will be processed in the order provided.',
59        ]

Get lines of text describing how this source is loaded. This text is intended to be used in a help prompt.

class ENVSpec(ConfigSourceSpec):
61class ENVSpec(ConfigSourceSpec):
62    """ A source spec for loading from an environmental variable. """
63
64    label: str = 'environmental variable'
65
66    def get_help_lines(self) -> typing.List[str]:
67        return [
68            'Environmental Variables',
69            'Load configuration values specified in environmental variables.',
70            f"Variable names must start with the prefix: '{edq.config.common._env_prefix}'.",
71            'Variable names will be stripped of the above prefix and turned to lower case.',
72        ]

A source spec for loading from an environmental variable.

label: str = 'environmental variable'

A label set by each subclass to identify the type of source spec. Generally, a user-friendly version of the class name.

def get_help_lines(self) -> List[str]:
66    def get_help_lines(self) -> typing.List[str]:
67        return [
68            'Environmental Variables',
69            'Load configuration values specified in environmental variables.',
70            f"Variable names must start with the prefix: '{edq.config.common._env_prefix}'.",
71            'Variable names will be stripped of the above prefix and turned to lower case.',
72        ]

Get lines of text describing how this source is loaded. This text is intended to be used in a help prompt.

class AbstractPathSpec(ConfigSourceSpec):
 74class AbstractPathSpec(ConfigSourceSpec):
 75    """
 76    An abstract source spec that includes a target directory and filename.
 77
 78    An argument not set (or set to None) indicates that the value should be determined at parse time.
 79    These values (generally paths or filenames) are often pulled from the `edq.config.settings` module
 80    (and therefore can be set by users of this library anytime before loading the config).
 81    """
 82
 83    def __init__(self,
 84            base_dir: typing.Union[str, None] = None,
 85            filename: typing.Union[str, None] = None,
 86            **kwargs: typing.Any) -> None:
 87        super().__init__()
 88
 89        if ((base_dir is not None) and (os.path.isfile(base_dir))):
 90            raise ValueError(f"The specified config dir is not a directory: '{base_dir}'.")
 91
 92        self.base_dir: typing.Union[str, None] = base_dir
 93        """ The directory to look for the global config file in. """
 94
 95        self.filename: typing.Union[str, None] = filename
 96        """ The filename to look for. """
 97
 98    def get_base_dir(self) -> typing.Union[str, None]:
 99        """ Get the base dir for this spec. """
100
101        if (self.base_dir is not None):
102            return self.base_dir
103
104        return self.get_default_base_dir()
105
106    def get_filename(self) -> typing.Union[str, None]:
107        """ Get the filename for this spec. """
108
109        if (self.filename is not None):
110            return self.filename
111
112        return self.get_default_filename()
113
114    def get_default_base_dir(self) -> typing.Union[str, None]:
115        """ Retrieve the default base dir (usually from edq.config.settings) for use when no base dir is explicitly set. """
116
117        return None
118
119    def get_default_filename(self) -> typing.Union[str, None]:
120        """ Retrieve the default filename (usually from edq.config.settings) for use when no filename is explicitly set. """
121
122        return None
123
124    def resolve_path(self,
125            override_path: typing.Union[str, None] = None,
126            ) -> str:
127        """
128        Get the path that should be use to load config.
129        This method is free to traverse the disk to find the desired path.
130        """
131
132        if (override_path is not None):
133            return override_path
134
135        base_dir = self.get_base_dir()
136        if (base_dir is None):
137            raise ValueError(f"Unable to resolve a config base dir for class {type(self)}.")
138
139        filename = self.get_filename()
140        if (filename is None):
141            raise ValueError(f"Unable to resolve a config filename for class {type(self)}.")
142
143        return os.path.join(base_dir, filename)

An abstract source spec that includes a target directory and filename.

An argument not set (or set to None) indicates that the value should be determined at parse time. These values (generally paths or filenames) are often pulled from the edq.config.settings module (and therefore can be set by users of this library anytime before loading the config).

base_dir: Optional[str]

The directory to look for the global config file in.

filename: Optional[str]

The filename to look for.

def get_base_dir(self) -> Optional[str]:
 98    def get_base_dir(self) -> typing.Union[str, None]:
 99        """ Get the base dir for this spec. """
100
101        if (self.base_dir is not None):
102            return self.base_dir
103
104        return self.get_default_base_dir()

Get the base dir for this spec.

def get_filename(self) -> Optional[str]:
106    def get_filename(self) -> typing.Union[str, None]:
107        """ Get the filename for this spec. """
108
109        if (self.filename is not None):
110            return self.filename
111
112        return self.get_default_filename()

Get the filename for this spec.

def get_default_base_dir(self) -> Optional[str]:
114    def get_default_base_dir(self) -> typing.Union[str, None]:
115        """ Retrieve the default base dir (usually from edq.config.settings) for use when no base dir is explicitly set. """
116
117        return None

Retrieve the default base dir (usually from edq.config.settings) for use when no base dir is explicitly set.

def get_default_filename(self) -> Optional[str]:
119    def get_default_filename(self) -> typing.Union[str, None]:
120        """ Retrieve the default filename (usually from edq.config.settings) for use when no filename is explicitly set. """
121
122        return None

Retrieve the default filename (usually from edq.config.settings) for use when no filename is explicitly set.

def resolve_path(self, override_path: Optional[str] = None) -> str:
124    def resolve_path(self,
125            override_path: typing.Union[str, None] = None,
126            ) -> str:
127        """
128        Get the path that should be use to load config.
129        This method is free to traverse the disk to find the desired path.
130        """
131
132        if (override_path is not None):
133            return override_path
134
135        base_dir = self.get_base_dir()
136        if (base_dir is None):
137            raise ValueError(f"Unable to resolve a config base dir for class {type(self)}.")
138
139        filename = self.get_filename()
140        if (filename is None):
141            raise ValueError(f"Unable to resolve a config filename for class {type(self)}.")
142
143        return os.path.join(base_dir, filename)

Get the path that should be use to load config. This method is free to traverse the disk to find the desired path.

class GlobalSpec(AbstractPathSpec):
145class GlobalSpec(AbstractPathSpec):
146    """ A source spec for loading from a global (user-level) config file. """
147
148    label: str = 'global config file'
149
150    def get_help_lines(self) -> typing.List[str]:
151        return [
152            'Global Config File',
153            'Load configurations from a "global" config file (e.g., a file that stays the same for a specific user).',
154            'This file is a good location for configuration values that do not change from project to project,',
155            'such as usernames and passwords/tokens.',
156            f"For this machine and user, the global config is located at: '{self.resolve_path()}'.",
157        ]
158
159    def get_default_base_dir(self) -> typing.Union[str, None]:
160        return edq.config.common._global_dir
161
162    def get_default_filename(self) -> typing.Union[str, None]:
163        return edq.config.common._config_filename

A source spec for loading from a global (user-level) config file.

label: str = 'global config file'

A label set by each subclass to identify the type of source spec. Generally, a user-friendly version of the class name.

def get_help_lines(self) -> List[str]:
150    def get_help_lines(self) -> typing.List[str]:
151        return [
152            'Global Config File',
153            'Load configurations from a "global" config file (e.g., a file that stays the same for a specific user).',
154            'This file is a good location for configuration values that do not change from project to project,',
155            'such as usernames and passwords/tokens.',
156            f"For this machine and user, the global config is located at: '{self.resolve_path()}'.",
157        ]

Get lines of text describing how this source is loaded. This text is intended to be used in a help prompt.

def get_default_base_dir(self) -> Optional[str]:
159    def get_default_base_dir(self) -> typing.Union[str, None]:
160        return edq.config.common._global_dir

Retrieve the default base dir (usually from edq.config.settings) for use when no base dir is explicitly set.

def get_default_filename(self) -> Optional[str]:
162    def get_default_filename(self) -> typing.Union[str, None]:
163        return edq.config.common._config_filename

Retrieve the default filename (usually from edq.config.settings) for use when no filename is explicitly set.

class LocalSpec(AbstractPathSpec):
165class LocalSpec(AbstractPathSpec):
166    """ A source spec for loading from a local config file. """
167
168    label: str = 'local config file'
169
170    def get_help_lines(self) -> typing.List[str]:
171        return [
172            'Local Config File',
173            'Load configurations from a "local" config file inside of the current directory.',
174            f"The file must be named: '{self.get_filename()}'.",
175        ]
176
177    def get_default_base_dir(self) -> typing.Union[str, None]:
178        return os.path.abspath(os.getcwd())
179
180    def get_default_filename(self) -> typing.Union[str, None]:
181        return edq.config.common._config_filename

A source spec for loading from a local config file.

label: str = 'local config file'

A label set by each subclass to identify the type of source spec. Generally, a user-friendly version of the class name.

def get_help_lines(self) -> List[str]:
170    def get_help_lines(self) -> typing.List[str]:
171        return [
172            'Local Config File',
173            'Load configurations from a "local" config file inside of the current directory.',
174            f"The file must be named: '{self.get_filename()}'.",
175        ]

Get lines of text describing how this source is loaded. This text is intended to be used in a help prompt.

def get_default_base_dir(self) -> Optional[str]:
177    def get_default_base_dir(self) -> typing.Union[str, None]:
178        return os.path.abspath(os.getcwd())

Retrieve the default base dir (usually from edq.config.settings) for use when no base dir is explicitly set.

def get_default_filename(self) -> Optional[str]:
180    def get_default_filename(self) -> typing.Union[str, None]:
181        return edq.config.common._config_filename

Retrieve the default filename (usually from edq.config.settings) for use when no filename is explicitly set.

class ProjectSpec(AbstractPathSpec):
183class ProjectSpec(AbstractPathSpec):
184    """
185    A source spec for loading from a project, which will look in the current directory and all parents until a matching file is found.
186    """
187
188    label: str = 'project config file'
189
190    def get_help_lines(self) -> typing.List[str]:
191        return [
192            'Project Config File',
193            'Load configurations from a "project" config file.',
194            'To find a project configuration file, we will look in the current directory and all parents (until root).',
195            'The first file found with the correct name will be loaded.',
196            'It is recommended to keep this file in your project\'s root directory.',
197            'This allows you to execute commands from anywhere in the project using the correct configuration settings.',
198            f"The file must be named: '{self.get_filename()}'.",
199        ]
200
201    def __init__(self,
202            root_cutoff: typing.Union[str, None] = None,
203            filename: typing.Union[str, None] = None,
204            **kwargs: typing.Any) -> None:
205        super().__init__(**kwargs)
206
207        self.root_cutoff: typing.Union[str, None] = root_cutoff
208        """ If set, stop searching parents if this directory is encountered. """
209
210    def get_default_base_dir(self) -> typing.Union[str, None]:
211        return os.path.abspath(os.getcwd())
212
213    def get_default_filename(self) -> typing.Union[str, None]:
214        return edq.config.common._config_filename
215
216    def resolve_path(self,
217            override_path: typing.Union[str, None] = None,
218            ) -> str:
219        if (override_path is not None):
220            return override_path
221
222        base_dir = self.get_base_dir()
223        if (base_dir is None):
224            raise ValueError(f"Unable to resolve a config base dir for class {type(self)}.")
225
226        filename = self.get_filename()
227        if (filename is None):
228            raise ValueError(f"Unable to resolve a config filename for class {type(self)}.")
229
230        current_dir = base_dir
231        for _ in range(edq.util.dirent.DEPTH_LIMIT):
232            path = os.path.join(current_dir, filename)
233            if (os.path.isfile(path)):
234                return path
235
236            # Check if current directory is root.
237            parent_dir = os.path.dirname(current_dir)
238            if (parent_dir == current_dir):
239                break
240
241            if ((self.root_cutoff is not None) and (self.root_cutoff == current_dir)):
242                break
243
244            current_dir = parent_dir
245
246        # If no file was found, use the base dir.
247        return os.path.join(base_dir, filename)

A source spec for loading from a project, which will look in the current directory and all parents until a matching file is found.

ProjectSpec( root_cutoff: Optional[str] = None, filename: Optional[str] = None, **kwargs: Any)
201    def __init__(self,
202            root_cutoff: typing.Union[str, None] = None,
203            filename: typing.Union[str, None] = None,
204            **kwargs: typing.Any) -> None:
205        super().__init__(**kwargs)
206
207        self.root_cutoff: typing.Union[str, None] = root_cutoff
208        """ If set, stop searching parents if this directory is encountered. """
label: str = 'project config file'

A label set by each subclass to identify the type of source spec. Generally, a user-friendly version of the class name.

def get_help_lines(self) -> List[str]:
190    def get_help_lines(self) -> typing.List[str]:
191        return [
192            'Project Config File',
193            'Load configurations from a "project" config file.',
194            'To find a project configuration file, we will look in the current directory and all parents (until root).',
195            'The first file found with the correct name will be loaded.',
196            'It is recommended to keep this file in your project\'s root directory.',
197            'This allows you to execute commands from anywhere in the project using the correct configuration settings.',
198            f"The file must be named: '{self.get_filename()}'.",
199        ]

Get lines of text describing how this source is loaded. This text is intended to be used in a help prompt.

root_cutoff: Optional[str]

If set, stop searching parents if this directory is encountered.

def get_default_base_dir(self) -> Optional[str]:
210    def get_default_base_dir(self) -> typing.Union[str, None]:
211        return os.path.abspath(os.getcwd())

Retrieve the default base dir (usually from edq.config.settings) for use when no base dir is explicitly set.

def get_default_filename(self) -> Optional[str]:
213    def get_default_filename(self) -> typing.Union[str, None]:
214        return edq.config.common._config_filename

Retrieve the default filename (usually from edq.config.settings) for use when no filename is explicitly set.

def resolve_path(self, override_path: Optional[str] = None) -> str:
216    def resolve_path(self,
217            override_path: typing.Union[str, None] = None,
218            ) -> str:
219        if (override_path is not None):
220            return override_path
221
222        base_dir = self.get_base_dir()
223        if (base_dir is None):
224            raise ValueError(f"Unable to resolve a config base dir for class {type(self)}.")
225
226        filename = self.get_filename()
227        if (filename is None):
228            raise ValueError(f"Unable to resolve a config filename for class {type(self)}.")
229
230        current_dir = base_dir
231        for _ in range(edq.util.dirent.DEPTH_LIMIT):
232            path = os.path.join(current_dir, filename)
233            if (os.path.isfile(path)):
234                return path
235
236            # Check if current directory is root.
237            parent_dir = os.path.dirname(current_dir)
238            if (parent_dir == current_dir):
239                break
240
241            if ((self.root_cutoff is not None) and (self.root_cutoff == current_dir)):
242                break
243
244            current_dir = parent_dir
245
246        # If no file was found, use the base dir.
247        return os.path.join(base_dir, filename)

Get the path that should be use to load config. This method is free to traverse the disk to find the desired path.