Search code examples
pythonrefactoringmagic-methods

Refactoring a Python class to take a dynamic parameter without changing calling pattern dramatically


I have a Python class in a base_params.py module within an existing codebase, which looks like this:

import datetime

class BaseParams:
    TIMESTAMP = datetime.datetime.now()
    PATH1 = f'/foo1/bar/{TIMESTAMP}/baz'
    PATH2 = f'/foo2/bar/{TIMESTAMP}/baz'

Callers utilize it this way:

from base_params import BaseParams as params

print(params.PATH1)

Now, I want to replace the TIMESTAMP value with one that is dynamically specified at runtime (through e.g. CLI arguments).

Is there a way to do this in Python without requiring my callers to refactor their code in a dramatic way? This is currently confounding me because the contents of the class BaseParams get executed at 'compile' time, so there is no opportunity there to pass in a dynamic value as it's currently structured. And in some of my existing code, this object is being treated as "fully ready" at 'compile' time, for example, its values are used as function argument defaults:

def some_function(value1, value2=params.PATH1):
    ...

I am wondering if there is some way to work with Python modules and/or abuse Python's __special_methods__ to get this existing code pattern working more or less as-is, without a deeper refactoring of some kind.

My current expectation is "this is not really possible" because of that last example, where the default value is being specified in the function signature. But I thought I should check with the Python wizards to see if there may be a suitably Pythonic way around this.


Solution

  • Yes, you just need to make sure that the command line argument is parsed before the class is defined and before any function that uses the class's attribute as a default argument is defined (but that should already be the case).

    (using sys.argv for sake of simplicity. It is better to use an actual argument parser such as argparse)

    import datetime
    import sys
    
    class BaseParams:
        try:
            TIMESTAMP = sys.argv[1]
        except IndexError:
            TIMESTAMP = datetime.datetime.now()
        PATH1 = f'/foo1/bar/{TIMESTAMP}/baz'
        PATH2 = f'/foo2/bar/{TIMESTAMP}/baz'
    
    
    print(BaseParams.TIMESTAMP)
    

    $ python main.py dummy-argument-from-cli
    

    outputs

    dummy-argument-from-cli
    

    while

    $ python main.py
    

    outputs

    2021-06-26 02:32:12.882601