0

I have a Kivy app that has text fields who's values I want to back in a dictionary (and persist by storing on disk as a JSON save file). I am having some trouble with this architecture, namely updating a value in the nested dictionary that backs the options in the UI.

data

{
   'general': 
      {
         'notes': '<notes>',
         'shift': 'NOC',
         'runs': 4
      },
   'notes':
      {
         'feeders': [<values>],
         'fluids': [<values>]
     }
}

Using Kivy, I have UI text boxes associated with each leaf key in the nested dictionary, and need to update the leaf key when the corresponding text box value is changed.

For example, when my text box for "General Notes" is updated, I need to update the value stored in data['general']['notes'].

In Kivy, I have for myapp.py:

class TextEntry(TextInput):

    default_text = StringProperty("Enter a value..")
       
    def on_text(self, instance, value):
        <data> = value if value is not self.default_text else <data>

    def text_init(self):
        return <data> if not (<data> is self.default_text or <data> == "") else self.default_text

Anywhere you see <data>, I need to know a specific key for a specific value in data, but <data> will vary for each instance of TextEntry.

and in my.kv I have:

<Some Layout>
  Label:
    text: 'Note'
  TextEntry:
    default_text: "Enter notes..."
    text: self.text_init()

The thing I'm struggling with, is that the class TextEntry is independent of the <data> key/value pair, so for any instance of TextEntry I do not know the specific key/value pair for <data> for that instance.

The trouble is this:

  • I can't simply store a reference to the value of <data> in a variable data_val on the instance and pass the reference around because the id(<data>) is different from any data_val= <data>; id(data_val).
  • Unlike a list slice object, a dictionary key can't be stored as an object (that always returns the value for that key when passed to mydict[<keyobject>]), so that I can't simply store a key object in a variable data_key on the instance and pass that around either.

Am I going about this wrong using a dictionary to back values in the UI? Or is there a way to do this with a dictionary that I'm not seeing? Or possibly, is this a silly architecture and there is a better way to handle these kinds of transactions that I'm not aware of?

Thank you for help and pointers!

1 Answers1

0

I ended up using this in my solution.

Here's what I did:

locate.py

from functools import reduce
import operator

def json_get(data: 'dict', address: 'tuple') -> 'object':
    ''' Get a value from a json (dict) object by address.

        Parameters:
            data:       json object (Python dict).
            address:    address location represented by a tuple of keys and indices.

        Returns the value at the address.
    '''
    
    return reduce(operator.getitem, address, data)


def json_set(data: 'dict', address: 'tuple', value: 'object') -> None:
    ''' Set a value of a json (dict) object by address.

        Parametes:
            data:       json object (Python dict).
            address:    address location represented by a tuple of keys and indices.
            value:      value to set at addressed location.
    '''

    json_get(data, address[:-1])[address[-1]] = value

if __name__ == '__main__':
    pass

myapp.py

import os
import json

from kivy.app import App
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.textinput import TextInput

from kivy.storage.jsonstore import JsonStore

from kivy.properties import ObjectProperty

from kivy.clock import Clock

from locate import json_get, json_set


workspace = os.getcwd()
with open(os.path.join(workspace, 'test.json'), 'r') as stream:
    json_data = json.load(stream)


class MyApp(App):

    def build(self):
        return MainWindow()


class MainWindow(AnchorLayout):

    data = ObjectProperty(json_data)


class TextEntry(TextInput):

    data = ObjectProperty(json_data)

    def on_focus(self, instance, value):
        if value:
            Clock.schedule_once(lambda dt: self.select_all())
        else:
            with open(os.path.join(workspace, 'test.json'), 'w') as stream:
                json.dump(self.data, stream)

    def on_text(self, instance, value):
        json_set(self.data, self.address, value)

    def text_init(self):
        return str(json_get(self.data, self.address))
        

if __name__ == '__main__':
    MyApp().run()

my.kv

#:kivy 2.0.0

<MainWindow>:
    TextEntry:
        address: ('notes', 'feeders')
        text: self.text_init()

Test it on test.json

{"general": {}, "notes": {"feeders": "John, Lindsey", "fluids": "Jason"}}

if you've got any suggestions for a better architecture or improvements, please let me know!