-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optional TOML configs support #2457
Changes from 24 commits
d00f793
783ddeb
fd4e30f
d712827
bcb59e0
0242248
debea65
ddfb5be
d0caa72
3261393
edf41c8
1d0d110
6142834
d86414a
b88376a
f4c128a
ba20f7b
22b7368
95fa46b
913fe19
ae050b0
284cc26
0d6943a
a7047fa
1c9129b
3843761
09ace16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,35 @@ | ||
Configuration | ||
============= | ||
|
||
All configuration can be done by adding configuration files. They are looked for in: | ||
All configuration can be done by adding configuration files. | ||
|
||
* ``/etc/luigi/client.cfg`` | ||
* ``luigi.cfg`` (or its legacy name ``client.cfg``) in your current working directory | ||
* ``LUIGI_CONFIG_PATH`` environment variable | ||
Supported config parsers: | ||
* ``cfg`` (default) | ||
* ``toml`` | ||
|
||
in increasing order of preference. The order only matters in case of key conflicts (see docs for ConfigParser.read_). These files are meant for both the client and ``luigid``. If you decide to specify your own configuration you should make sure that both the client and ``luigid`` load it properly. | ||
You can choose right parser via ``LUIGI_CONFIG_PARSER`` environment variable. For example, ``LUIGI_CONFIG_PARSER=toml``. | ||
|
||
Default (cfg) parser are looked for in: | ||
|
||
* ``/etc/luigi/client.cfg`` (deprecated) | ||
* ``/etc/luigi/luigi.cfg`` | ||
* ``client.cfg`` (deprecated) | ||
* ``luigi.cfg`` | ||
* ``LUIGI_CONFIG_PATH`` environment variable | ||
|
||
TOML parser are looked for in: | ||
|
||
* ``/etc/luigi/luigi.toml`` | ||
* ``luigi.toml`` | ||
* ``LUIGI_CONFIG_PATH`` environment variable | ||
|
||
Both config lists reversordered by priotity (from low to high). The order only matters in case of key conflicts (see docs for ConfigParser.read_). These files are meant for both the client and ``luigid``. If you decide to specify your own configuration you should make sure that both the client and ``luigid`` load it properly. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
.. _ConfigParser.read: https://docs.python.org/3.6/library/configparser.html#configparser.ConfigParser.read | ||
|
||
The config file is broken into sections, each controlling a different part of the config. Example configuration file: | ||
The config file is broken into sections, each controlling a different part of the config. | ||
|
||
Example cfg config: | ||
|
||
.. code:: ini | ||
|
||
|
@@ -23,6 +40,17 @@ The config file is broken into sections, each controlling a different part of th | |
[core] | ||
scheduler_host=luigi-host.mycompany.foo | ||
|
||
Example toml config: | ||
|
||
.. code:: python | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because Pygments doesn't have available lexerfor TOML. This is already contributed in some Pygments versions: But Pygments which used for travis checks doesn't have this lexer yet. I choose python as alternative lexer because toml syntax very similar to hybrid of python and ini :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And a TODO then please. :) |
||
|
||
[hadoop] | ||
version = "cdh4" | ||
streaming-jar = "/usr/lib/hadoop-xyz/hadoop-streaming-xyz-123.jar" | ||
|
||
[core] | ||
scheduler_host = "luigi-host.mycompany.foo" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are single quotes acceptable too? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, see TOML spec for more details about syntax There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe link to the spec in the docs too? |
||
|
||
|
||
.. _ParamConfigIngestion: | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright 2012-2015 Spotify AB | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't these dates include 2018. Probably not that important There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just copied Apache license notice from original configuration.py. You can improve it, I don't now how it must be look in right way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea I think this is fine ... |
||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
from .cfg_parser import LuigiConfigParser | ||
from .core import get_config, add_config_path | ||
from .toml_parser import LuigiTomlParser | ||
|
||
|
||
__all__ = [ | ||
'add_config_path', | ||
'get_config', | ||
'LuigiConfigParser', | ||
'LuigiTomlParser', | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright 2012-2015 Spotify AB | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
import logging | ||
|
||
|
||
# IMPORTANT: don't inherit from `object`! | ||
# ConfigParser have some troubles in this case. | ||
class BaseParser: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. inherit from object? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No-no-no, don't do it! In Python2 cfg parser fails if BaseParser inherit from object. This is strange, yes. I will add comment for it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue is explained better on SO |
||
@classmethod | ||
def instance(cls, *args, **kwargs): | ||
""" Singleton getter """ | ||
if cls._instance is None: | ||
cls._instance = cls(*args, **kwargs) | ||
loaded = cls._instance.reload() | ||
logging.getLogger('luigi-interface').info('Loaded %r', loaded) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. side note: am i correct that neither the client nor the server's logging config names are configurable? I've found that interesting - why statically set to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I was just raising an unrelated question - a topic for future possible PR (configurable logging names). Nothing for you to do here. |
||
|
||
return cls._instance | ||
|
||
@classmethod | ||
def add_config_path(cls, path): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason for changing the order of these classmethods? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What did you mean? I just moved this method from cfg parser to base without any changes. |
||
cls._config_paths.append(path) | ||
cls.reload() | ||
|
||
@classmethod | ||
def reload(cls): | ||
return cls.instance().read(cls._config_paths) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,6 @@ | |
See :doc:`/configuration` for more info. | ||
""" | ||
|
||
import logging | ||
import os | ||
import warnings | ||
|
||
|
@@ -38,37 +37,19 @@ | |
except ImportError: | ||
from configparser import ConfigParser, NoOptionError, NoSectionError | ||
|
||
from .base_parser import BaseParser | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I recall relative imports are discouraged, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? We already have |
||
|
||
class LuigiConfigParser(ConfigParser): | ||
|
||
class LuigiConfigParser(BaseParser, ConfigParser): | ||
NO_DEFAULT = object() | ||
enabled = True | ||
_instance = None | ||
_config_paths = [ | ||
'/etc/luigi/client.cfg', # Deprecated old-style global luigi config | ||
'/etc/luigi/luigi.cfg', | ||
'client.cfg', # Deprecated old-style local luigi config | ||
'luigi.cfg', | ||
] | ||
if 'LUIGI_CONFIG_PATH' in os.environ: | ||
config_file = os.environ['LUIGI_CONFIG_PATH'] | ||
if not os.path.isfile(config_file): | ||
warnings.warn("LUIGI_CONFIG_PATH points to a file which does not exist. Invalid file: {path}".format(path=config_file)) | ||
else: | ||
_config_paths.append(config_file) | ||
|
||
@classmethod | ||
def add_config_path(cls, path): | ||
cls._config_paths.append(path) | ||
cls.reload() | ||
|
||
@classmethod | ||
def instance(cls, *args, **kwargs): | ||
""" Singleton getter """ | ||
if cls._instance is None: | ||
cls._instance = cls(*args, **kwargs) | ||
loaded = cls._instance.reload() | ||
logging.getLogger('luigi-interface').info('Loaded %r', loaded) | ||
|
||
return cls._instance | ||
|
||
@classmethod | ||
def reload(cls): | ||
|
@@ -124,10 +105,3 @@ def set(self, section, option, value=None): | |
ConfigParser.add_section(self, section) | ||
|
||
return ConfigParser.set(self, section, option, value) | ||
|
||
|
||
def get_config(): | ||
""" | ||
Convenience method (for backwards compatibility) for accessing config singleton. | ||
""" | ||
return LuigiConfigParser.instance() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright 2012-2015 Spotify AB | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
import logging | ||
import os | ||
import warnings | ||
|
||
from .cfg_parser import LuigiConfigParser | ||
from .toml_parser import LuigiTomlParser | ||
|
||
|
||
logger = logging.getLogger('luigi-interface') | ||
|
||
|
||
PARSERS = { | ||
'cfg': LuigiConfigParser, | ||
'conf': LuigiConfigParser, | ||
'ini': LuigiConfigParser, | ||
'toml': LuigiTomlParser, | ||
} | ||
|
||
# select parser via env var | ||
DEFAULT_PARSER = 'cfg' | ||
PARSER = os.environ.get('LUIGI_CONFIG_PARSER', DEFAULT_PARSER) | ||
if PARSER not in PARSERS: | ||
warnings.warn("Invalid parser: {parser}".format(parser=PARSER)) | ||
PARSER = DEFAULT_PARSER | ||
|
||
|
||
def get_config(parser=PARSER): | ||
"""Get configs singleton for parser | ||
""" | ||
|
||
parser_class = PARSERS[parser] | ||
if not parser_class.enabled: | ||
logger.error(( | ||
"Parser not installed yet. " | ||
"Please, install luigi with required parser:\n" | ||
"pip install luigi[{parser}]" | ||
).format(parser) | ||
) | ||
|
||
return parser_class.instance() | ||
|
||
|
||
def add_config_path(path): | ||
"""Select config parser by file extension and add path into parser. | ||
""" | ||
if not os.path.isfile(path): | ||
warnings.warn("Config file does not exist: {path}".format(path=path)) | ||
return False | ||
|
||
# select parser by file extension | ||
_base, ext = os.path.splitext(path) | ||
if ext and ext[1:] in PARSERS: | ||
parser_class = PARSERS[ext[1:]] | ||
else: | ||
parser_class = PARSERS[PARSER] | ||
|
||
# add config path to parser | ||
parser_class.add_config_path(path) | ||
return True | ||
|
||
|
||
if 'LUIGI_CONFIG_PATH' in os.environ: | ||
add_config_path(os.environ['LUIGI_CONFIG_PATH']) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright 2018 Cindicator Ltd. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
import os.path | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why import both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nearly. |
||
|
||
try: | ||
import toml | ||
except ImportError: | ||
toml = False | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @orsinium
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh noes, @orsinium can you fix this asap? |
||
|
||
from .base_parser import BaseParser | ||
|
||
|
||
class LuigiTomlParser(BaseParser): | ||
NO_DEFAULT = object() | ||
enabled = bool(toml) | ||
data = dict() | ||
_instance = None | ||
_config_paths = [ | ||
'/etc/luigi/luigi.toml', | ||
'luigi.toml', | ||
] | ||
|
||
@staticmethod | ||
def _update_data(data, new_data): | ||
if not new_data: | ||
return data | ||
if not data: | ||
return new_data | ||
for section, content in new_data.items(): | ||
if section not in data: | ||
data[section] = dict() | ||
data[section].update(content) | ||
return data | ||
|
||
def read(self, config_paths): | ||
self.data = dict() | ||
for path in config_paths: | ||
if os.path.isfile(path): | ||
self.data = self._update_data(self.data, toml.load(path)) | ||
return self.data | ||
|
||
def get(self, section, option, default=NO_DEFAULT, **kwargs): | ||
try: | ||
return self.data[section][option] | ||
except KeyError: | ||
if default is self.NO_DEFAULT: | ||
raise | ||
return default | ||
|
||
def getboolean(self, section, option, default=NO_DEFAULT): | ||
return self.get(section, option, default) | ||
|
||
def getint(self, section, option, default=NO_DEFAULT): | ||
return self.get(section, option, default) | ||
|
||
def getfloat(self, section, option, default=NO_DEFAULT): | ||
return self.get(section, option, default) | ||
|
||
def getintdict(self, section): | ||
return self.data.get(section, {}) | ||
|
||
def set(self, section, option, value=None): | ||
if section not in self.data: | ||
self.data[section] = {} | ||
self.data[section][option] = value | ||
|
||
def __getitem__(self, name): | ||
return self.data[name] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -488,7 +488,7 @@ def _get_s3_config(self, key=None): | |
defaults = dict(configuration.get_config().defaults()) | ||
try: | ||
config = dict(configuration.get_config().items('s3')) | ||
except NoSectionError: | ||
except (NoSectionError, KeyError): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Am i correct to assume that ConfigParser returns the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. I think, this is incorrect raise from toml parser any exceptions from ConfigParser. Maybe, we can make custom common exception: class SectionError(NoSectionError, KeyError):
pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with it as it is currently written; I was just verifying that they raised different errors for the same problem. |
||
return {} | ||
# So what ports etc can be read without us having to specify all dtypes | ||
for k, v in six.iteritems(config): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,6 +58,7 @@ def get_static_files(path): | |
license='Apache License 2.0', | ||
packages=[ | ||
'luigi', | ||
'luigi.configuration', | ||
'luigi.contrib', | ||
'luigi.contrib.hdfs', | ||
'luigi.tools' | ||
|
@@ -75,6 +76,9 @@ def get_static_files(path): | |
] | ||
}, | ||
install_requires=install_requires, | ||
extras_require={ | ||
'toml': ['toml'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are your thoughts on limiting this to the current milestone version? i.e. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Python toml package version related to toml spec version. TOML spec v1.0 will be backward compatible. Some quotes:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, why don't we put There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. This don't break anything. This fix added. Thank you for review :) |
||
}, | ||
classifiers=[ | ||
'Development Status :: 5 - Production/Stable', | ||
'Environment :: Console', | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we instead require that the file must end in
.toml
? It's very little magic for more convenience.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think about it, but this is very explicit while we have many possible places for configs. What if we have
luigi.cfg
andluigi.toml
? We can't combine it into one config.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can add an improvement down the road to define a hierarchical level of config extensions. I.e. (.cfg, .toml, .yml, <etc.>). I'm fine with requiring an ENV variable for now