How can I constantly check if a JSON file is updated in Python?

Viewed 3213

I'm making a clock using Tkinter and Python.

What I want to achieve is having the clock run and automatically check if the JSON file (which contains the clock's visual settings) has been updated. If the JSON file has been updated, then the clock will update live.

However, this isn't the case. What I have to do is update the JSON file, close the clock program, then re-open the clock program. Only in this case will the changed JSON settings take place.

clock.py

from tkinter import *
from datetime import datetime
from settings import *

# Updates the program and clock settings
def tick():
    time_string = datetime.now().strftime(time_format)
    date_string = datetime.now().strftime(date_format)

    root.config(bg=background_color)

    container.configure(bg=background_color)

    current_time.configure(text=time_string,
                           font=(time_font, time_size, time_weight, time_slant),
                           fg=time_color,
                           bg=background_color)

    current_date.configure(text=date_string,
                           font=(date_font, date_size, date_weight, date_slant),
                           fg=date_color,
                           bg=background_color)

    current_time.after(1, tick)


# TKInterface
root = Tk()
root.title(window_title)

# Binds 'Esc' key to exit program
root.bind('<Escape>', exit)

# Runs program in full-screen
if full_screen:
    root.attributes('-fullscreen', True)
    root.config(cursor='none')

# Creates container to hold time and date
container = Frame(root)
current_time = Label(container)
current_date = Label(container)

container.pack(expand=True)
current_time.pack()
current_date.pack()

tick()
root.mainloop()

settings.py

import os
import json

with open('settings.json') as json_settings:
    settings = json.load(json_settings)

    # Window
    full_screen = settings['window']['full_screen']
    window_title = settings['window']['window_title']

    # Background
    background_color = settings['background']['color']

    # Time
    time_font = settings['time']['font']
    time_size = settings['time']['size']
    time_weight = settings['time']['weight'] 
    time_slant = settings['time']['slant']
    time_color = settings['time']['color']    
    time_format = settings['time']['format']

    # Date
    date_font = settings['date']['font']
    date_size = settings['date']['size']
    date_weight = settings['date']['weight']
    date_slant = settings['date']['slant']
    date_color = settings['date']['color']
    date_format = settings['date']['format']

settings.json

{
    "window": {
      "full_screen": false,
      "window_title" : "chronoberry"
    },
    "background": {
      "color": "black"
    },
    "time": {
      "font": "arial",
      "size": 70,
      "weight": "bold",
      "slant": "roman",
      "color": "white",
      "format": "%-I:%M:%S %p"
    },
    "date": {
      "font": "arial",
      "size": 20,
      "weight": "normal",
      "slant": "roman",
      "color": "white",
      "format": "%A, %B %-d %Y"
    }
  }

Desired Effect:

If I change the background color in my JSON file and save it, my clock should be able to update its color during run-time.

Before changing the background color

After changing the background color

What I've Tried:

  • Re-importing the settings.py module using importlib.reload(), but the settings.py module isn't a valid argument.
  • Opening settings.json, reading from it, closing it, then opening it again. However, once a file is closed, it's impossible to open it again.
3 Answers

Your settings module is essentially a module of constants, which makes it quite hard to reuse. While you could do something hacky to force the reload like:

def tick():
    import settings, importlib
    importlib.reload(settings)
    from settings import *
    # ... rest of tick code ...

that's going to be both inefficient and a terrible misuse of the import mechanism (now instead of reading one file, you're reading two, the module and the JSON it depends on).

Instead, I'd recommend making settingss a little more reusable with a function that can read the data and cache it, rather than a bunch of globals:

import os
import json

class Settings:
    SETTINGS_FILE = 'settings.json'

    def __init__(self):
        self._load_settings()

    def update_settings(self):
        if self._last_update != os.stat(self.SETTINGS_FILE).st_mtime:
            self._load_settings()
            return True
        return False

    def _load_settings(self):
        with open(self.SETTINGS_FILE) as json_settings:
            settings = json.load(json_settings)
            self._last_update = os.fstat(json_settings.fileno()).st_mtime

            # Window
            self.full_screen = settings['window']['full_screen']
            self.window_title = settings['window']['window_title']

            # Background
            self.background_color = settings['background']['color']

            # Time
            self.time_font = settings['time']['font']
            self.time_size = settings['time']['size']
            self.time_weight = settings['time']['weight'] 
            self.time_slant = settings['time']['slant']
            self.time_color = settings['time']['color']    
            self.time_format = settings['time']['format']

            # Date
            self.date_font = settings['date']['font']
            self.date_size = settings['date']['size']
            self.date_weight = settings['date']['weight']
            self.date_slant = settings['date']['slant']
            self.date_color = settings['date']['color']
            self.date_format = settings['date']['format']

Now your clock module can import settings, construct a Settings object up front, and at each tick call update_settings() on it. If update_settings returns True, it should also reapply the configuration. The code would need to qualify the various names, so instead of just saying date_color, you'd put:

mysettings = Settings()

at the top level, and refer to date_color with:

mysettings.date_color

but that's a small price to pay to improve the code.

How serious is this code? You could just check for the file's last modified time at some interval with os.stat(path_to_file).st_mtime and refresh your interface if it's later than the last time you checked. Quick and dirty.

You have a couple of options. 1 is the option benas has suggested. Every second or so check the last modified time of the json file to see if it has changed.

Another option is to use a package such as PyiNotify. This package can identify events that occur on files and folders. 1 such event is the IN_MODIFY event which triggers when a file has been modified. A list of events can be found here.

Firstly you would need to install the package using pip:

pip install pyinotify

The following is example code (almost entirely from their documentation)

import pyinotify

# The watch manager stores the watches and provides operations on watches
wm = pyinotify.WatchManager()
wm.add_watch('/path/to/file', mask, rec=True)

mask = pyinotify.IN_MODIFY  # watched events

class EventHandler(pyinotify.ProcessEvent):
    def process_IN_MODIFY(self, event):
        print "Modifying:", event.pathname
Related