0

In advance: I apologize for the vague question and long description. I cannot for the life of me pinpoint the problem and I am now desperate.

This is a small program meant to run on the command line. It allows the user to add or update an entry. The entries are key-value pairs of a dictionary--which is initially a text file on the same level as the script named info.txt which must be instantiated with "{}" (lousy program, I know. I'll work out kinks later). In summation, it reads a text file, turns it into a JSON object for manipulation, then writes back into the text file.

The script:

import os
import json
import sys


def addNew():

    with open(os.path.join(os.path.abspath(os.path.dirname(__file__)),
              "info.txt")) as entryFile:
        entryDict = json.load(entryFile)

    newEntry = sys.argv[1]
    newValue = sys.argv[2]
    confirmNew = input("Add \"{0}\" with \"{1}\" to the dictionary?"
                       "\ny or n\n".format(newEntry, newValue))

    if confirmNew == "y":
        entryDict[newEntry] = newValue
        print("You have added {} to your dictionary".format(newEntry))
    else:
        print("You have not added a new entry")

    entryString = json.dumps(entryDict)

    with open(os.path.join(os.path.abspath(os.path.dirname(__file__)),
              "info.txt"), "r+") as entryFile:
        entryFile.write(entryString)


def update():
    print("An entry with this name already exists.")

    entryFile = open(os.path.join(os.path.abspath(os.path.dirname(__file__)),                     
        "info.txt"), "r+")

    entryDict = json.load(entryFile)
    confirmUpdate = input("Update '{0}' with '{1}'?\n"
                          .format(sys.argv[1], sys.argv[2]))
    if confirmUpdate == "y":
        entryDict.update({str(sys.argv[1]): sys.argv[2]})

    entryString = json.dumps(entryDict)
    entryFile.truncate(0)
    entryFile.write(entryString)
    entryFile.close()
    print("{} has been updated.".format(sys.argv[1]))


def main():

    entryFile = open(os.path.join(os.path.abspath(os.path.dirname(__file__)),         
    "info.txt"))
    entryDict = json.load(entryFile)
    entryFile.close()

    if len(sys.argv) < 2:
        print('usage: python3 {} entry value - entry manager '
              '\nentry: name of entry and its value to add to the dict.'
              .format(sys.argv[0]))
        sys.exit()

    if len(sys.argv) == 3 and not sys.argv[1] in entryDict:
        addNew()
        sys.exit()

    if len(sys.argv) == 3 and sys.argv[1] in entryDict:
        update()
        sys.exit()


if __name__ == "__main__":
    main()

It works as intended, until I call the update() function twice. In other words, it brings up a JSONDecodeError only when I do python3 file.py entryName valueHere twice. Twice, because per the program, if an entryName already exists in the info.txt dictionary, then it is supposed to update that entry.
Why does update() work once, but not twice?
The error: raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) points to entryDict = json.load(entryFile) within the main function.
I've tried different variations of load/loads, "r/w/a+". Nothing seems to work. What am I doing (mainly) wrong?

edit: any hints/tips towards general scripting/programming would be greatly appreciated.

Jay Jung
  • 1,805
  • 3
  • 23
  • 46
  • 1
    Some extra characters are being appended to your file during write: `>>> open("info.txt").read() '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00{"k": "v2"}'` – thaavik Aug 09 '17 at 17:09
  • @thaavik where the heck did that come from? And how do I stop it? – Jay Jung Aug 09 '17 at 17:11

1 Answers1

2

The problem is that after you call file.truncate(0), you don't write the new content to the start of the file. From the docs:

truncate(size=None)

Resize the stream to the given size in bytes (or the current position if size is not specified). The current stream position isn’t changed. This resizing can extend or reduce the current file size. In case of extension, the contents of the new file area depend on the platform (on most systems, additional bytes are zero-filled). The new file size is returned.

So after reading, truncating and writing to the file, the contents of your file will look somewhat like this (where \x00 denotes a null byte):

\x00\x00\x00\x00\x00\x00\x00\x00{"key": "value"}

To fix this, add entryFile.seek(0) after entryFile.truncate(0).

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • Yep, this is the right approach. See the first answer here for a cleaner way to open the file for reading and over-writing: https://stackoverflow.com/questions/6648493/open-file-for-both-reading-and-writing – thaavik Aug 09 '17 at 17:17