Skip to content
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

Fixing TOML logging configuration #2984

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Conversation

lphk92
Copy link

@lphk92 lphk92 commented Aug 14, 2020

Description

Using the logging section of a TOML configuration file is currently not working, because the logging module attempts to modify the given configuration dictionary in-place, and all parsed TOML configuration sections are converted to the immutable FrozenOrderedDict after being read. This was originally observed on python version 3.6

This change "thaws" the FrozenOrderedDict before passing it into the logging module.

Motivation and Context

I was trying to use a TOML configuration file for logging, as shown in https://github.com/spotify/luigi/blob/master/examples/config.toml
however, trying to use it resulted in the following error

Traceback (most recent call last):                                                                                                                                                                                                                            
  File "/Users/kushnerl/miniconda3/lib/python3.7/logging/config.py", line 543, in configure                                                                                                                                                                   
    formatters[name])                                                                                                                                                                                                                                         
TypeError: 'FrozenOrderedDict' object does not support item assignment                                                                                                                                                                                        
                                                                                                                                                                                                                                                              
The above exception was the direct cause of the following exception:                                                                                                                                                                                          
                                                                                                                                                                                                                                                              
Traceback (most recent call last):                                                                                                                                                                                                                            
 ...                                                                                                                                                                                                               File "/Users/kushnerl/miniconda3/lib/python3.7/site-packages/luigi/interface.py", line 237, in build                         
    luigi_run_result = _schedule_and_run(tasks, worker_scheduler_factory, override_defaults=env_params)                        
  File "/Users/kushnerl/miniconda3/lib/python3.7/site-packages/luigi/interface.py", line 145, in _schedule_and_run             
    InterfaceLogging.setup(env_params)                                                                                                                                                                                                                        
  File "/Users/kushnerl/miniconda3/lib/python3.7/site-packages/luigi/setup_logging.py", line 78, in setup                                                                                                                                                     
    configured = cls._section(opts)                                                                                                                                                                                                                           
  File "/Users/kushnerl/miniconda3/lib/python3.7/site-packages/luigi/setup_logging.py", line 43, in _section                                                                                                                                                  
    logging.config.dictConfig(logging_config)                                                                                                                                                                                                                 
  File "/Users/kushnerl/miniconda3/lib/python3.7/logging/config.py", line 800, in dictConfig                                                                                                                                                                  
    dictConfigClass(config).configure()                                                                                                                                                                                                                       
  File "/Users/kushnerl/miniconda3/lib/python3.7/logging/config.py", line 546, in configure                                    
    'formatter %r' % name) from e                                                                                              
ValueError: Unable to configure formatter 'default' 

@hirolau
Copy link
Contributor

hirolau commented Oct 6, 2020

For convenience here is a link to my initial bug-report of this issue (#2879). This solution is better then the simple one I suggested in the original issue.

I would very much want this merged. Thank you for contributing.

Edit: The freezing function goes through lists and tuples and freezes all items as well. If we have a tuple/list of FrozenDicts they will not thaw in the current implementation. Not sure if this is important for the logging setup scenario, but it will hurt someone somewhere down the line.

Here is a simple example of what I mean:

initial = {'x': [{'a':'a', 'b':'b'}]}
frozen = recursively_freeze(initial)
frozen['x'][0]['a'] = 'hello' # This fails, as it should!
thawed = recursively_thaw(frozen)
thawed['x'][0]['a'] = 'hello' # This shuold work but does not.

I suggest we add a check for tuples and recursively convert them to lists, something like this:

def recursively_thaw(value):
    """
    Recursively walks the given ``FrozenOrderedDict``, converting all ``FrozenOrderedDict`` objects into ``dict``. Tuples will be converted to lists, even if they were initially tuples when converting to ``FrozenOrderedDict``
    """
    if isinstance(value, FrozenOrderedDict):
        value = dict(value)
        
    if isinstance(value, dict):
        return {k: recursively_thaw(v) for k, v in value.items()}
    elif isinstance(value, tuple):
        return [recursively_thaw(item) for item in value]
    
    return value

@lphk92
Copy link
Author

lphk92 commented Oct 6, 2020

Great catch @hirolau! I just pushed up another commit that fixes the issue.

@hirolau
Copy link
Contributor

hirolau commented Oct 7, 2020

One more very very small thing. When I read the function the first time to added the line for tuples my mind read the conditions as elif instead of if. This was because I just assumed it would be a more pure recursive function, and my mind has a habit of assuming things. Thus my first attempt to change the function was a failure and I had to go back and debug. If I were to write this function I would do it like this with a single condition so the mind can be sure only one statement is run per recursion.

I am fine with either implementation, but if you like my take better feel free to change it.

def recursively_thaw(value):
    """
    Recursively walks the given ``FrozenOrderedDict``, converting all ``FrozenOrderedDict`` objects into ``dict``. 
    Tuples will be converted to lists, even if they were initially tuples when converting to ``FrozenOrderedDict``.
    """
    if isinstance(value, FrozenOrderedDict):
        return recursively_thaw(dict(value))
    
    elif isinstance(value, dict):
        return {key: recursively_thaw(val) for key, val in value.items()}
    
    elif isinstance(value, tuple):
        return [recursively_thaw(item) for item in value]
    
    else:
        return value

Copy link
Collaborator

@dlstadther dlstadther left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes make sense to me. however, are there any tests which could be added to display the fix to the current issue at hand?

@stale
Copy link

stale bot commented Jan 9, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If closed, you may revisit when your time allows and reopen! Thank you for your contributions.

@stale stale bot added the wontfix label Jan 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants