7
\$\begingroup\$

I made a simple program to catalog some old records I have. It seems a tad redundant in the searching function. Does anyone know what I can do about that?

import easygui as eg
import sys

namedoc = open(r"C:\Users\User\Desktop\RcrdCat\names.txt", 'a')
nd2 = open(r"C:\Users\User\Desktop\RcrdCat\names(2).txt", 'a')
authdoc = open(r"C:\Users\User\Desktop\RcrdCat\authors.txt", 'a')
yeardoc = open(r"C:\Users\User\Desktop\RcrdCat\dates.txt", 'a')
pubdoc = open(r"C:\Users\User\Desktop\RcrdCat\pubs.txt", 'a')
rpmdoc = open(r"C:\Users\User\Desktop\RcrdCat\rpms.txt", 'a')
conddoc = open(r"C:\Users\User\Desktop\RcrdCat\conditions.txt", 'a')
sleevedoc = open(r"C:\Users\User\Desktop\RcrdCat\sleeves.txt", 'a')

doclist = [namedoc, yeardoc, pubdoc, rpmdoc, conddoc, sleevedoc]

def getlen(doc):
    templist = doc.readlines()
    listlen = len(templist)
    templist = []
    return listlen

def mainmenus():
    mm = eg.buttonbox("What would you like to do?", "Categorizer", ["Add Records", "Search Records", "Exit"])
    if mm == "Exit":
        exitprgm()
    elif mm == "Add Records":
        addrecs()
    elif mm == "Search Records":
        searchrecs(getsearchterms())

def addrecs():
    info = eg.multenterbox("Please enter all record info", "Add Records", ["Name", "Year", "Publisher", "RPM", "Condition", "Sleeve", "Name (2)", "Artist"])
    if info == None:
        mainmenus()
    else:
        namedoc.write(info[0] + " \r\n")
        yeardoc.write(info[1] + " \r\n")
        pubdoc.write(info[2] + " \r\n")
        rpmdoc.write(info[3] + " \r\n")
        conddoc.write(info[4] + " \r\n")
        nd2.write(info[6] + " \r\n")
        authdoc.write(info[7] + " \r\n")
        if info[5] == "Yes" or info[5] == "No":
            sleevedoc.write(info[5] + " \r\n")
        else:
            eg.msgbox("Please enter \"Yes\" or \"No\"")
            addrecs()
        addrecs()

def getsearchterms():
    term = eg.enterbox("Please enter your term in the following way: the word \"name\", \"year\", \"pub\", \"rpm\", \"cond\", or \"sleeve\", then a space, then the corresponding value")
    try:
        term = term.split()
        return term
    except AttributeError:
        mainmenus()

def searchrecs(term):
    hits = []
    if term[0] == "name":
        myrange = getlen(namedoc)
        for number in range(getlen(namedoc)):
            locstring = namedoc.readlines(number)
            if term[1] in locstring == True:
                hits.append(number)
            else:
                pass
    elif term[0] == "year":
        for number in range(getlen(yeardoc)):
            locstring = yeardoc.readlines(number)
            if term[1] in locstring == True:
                hits.append(number)
            else:
                pass
    elif term[0] == "pub":
        for number in range(getlen(pubdoc)):
            locstring = pubdoc.readlines(number)
            if term[1] in locstring == True:
                hits.append(number)
            else:
                pass
    elif term[0] == "rpm":
        for number in range(getlen(rpmdoc)):
            locstring = rpmdoc.readlines(number)
            if term[1] in locstring == True:
                hits.append(number)
            else:
                pass
    elif term[0] == "cond":
        for number in range(getlen(conddoc)):
            locstring = conddoc.readlines(number)
            if term[1] in locstring == True:
                hits.append(number)
            else:
                pass
    elif term[0] == "sleeve":
        for number in range(getlen(sleevedoc)):
            locstring = sleevedoc.readlines(number)
            if term[1] in locstring == True:
                hits.append(number)
            else:
                pass
    else:
        eg.msgbox("Please enter valid search criteria")

    hitnums = len(hits)
    allinfo = []
    eg.msgbox("Found " + str(hitnums) + " hits. Click OK to view.")
    for number in hits:
        for doc in doclist:
            allinfo.append(doc.readlines(number))
        mation = ["Name: " + allinfo[0], "Year: " + allinfo[1], "Publisher: " + allinfo[2], "RPM: " + allinfo[3],"Condition: " + allinfo[4],"Sleeve: " + allinfo[5]]
        ftext = str()
        for num in range(len(mation)):
            ftext = ftext + num + " "
        eg.textbox("", "Results", ftext)

def exitprgm():
    for file in doclist:
        file.write("\r\n")
        file.close()
    sys.exit()

mainmenus()
\$\endgroup\$
  • 1
    \$\begingroup\$ I think you may be confused about how readlines works stackoverflow.com/questions/14541010/… \$\endgroup\$ – Stuart Jan 21 '14 at 0:52
  • \$\begingroup\$ @stuart lol, you are totally right. Still, it doesn't seem that this would affect the redundancy of the list. I just have to run readlines() first to get a table and iterate over that :). \$\endgroup\$ – KnightOfNi Jan 21 '14 at 1:06
  • \$\begingroup\$ Don't you get an error trying to read from files opened in 'a' mode? \$\endgroup\$ – Janne Karila Jan 21 '14 at 7:55
  • \$\begingroup\$ @JanneKarila Guilty... Originally wrote this so they opened in 'r+'. However, I wanted to append to files instead of overwriting them so I just don't use search... If I wanted to I would probably use with/as and the 'a' mode for writing and make them open as 'r' for reading, but I never use the search function anyway \$\endgroup\$ – KnightOfNi Jan 21 '14 at 19:10
  • \$\begingroup\$ Have you considered using SQLite? \$\endgroup\$ – Joel Cornett Jan 22 '14 at 16:51
3
\$\begingroup\$

So I've got a little disclaimer. I'm not at a pc with python on it.

A quick look would sugggest an approach like:

def searchrecs(term):
    hits = []

    searchterms = {
        "name": namedoc,
        "year": yeardoc,
        "pub": pubdoc,
    }

    if(term[0] in searchterms.keys()):
        for searchterm, searchdoc in searchterms:
            if term[0] == searchterm:
                myrange = getlen(searchdoc)
                for number in range(getlen(searchdoc)):
                    locstring = searchdoc.readlines(number)
                    if term[1] in locstring == True:
                        hits.append(number)
                    else:
                        pass
    else:
        eg.msgbox("Please enter valid search criteria")

Forgive any c# syntax in this. I will fix any errors as I found them.

\$\endgroup\$
  • \$\begingroup\$ Very interesting. I like the if/in usage. Much more efficient than my original code, thanks very much! \$\endgroup\$ – KnightOfNi Jan 21 '14 at 1:35
4
\$\begingroup\$

Another piece of python wisdom to consider is 'if you can do it with the standard library, do!'

Field and record based storage is really easy to do with the csv module, which reads and writes comma-separated spreadsheet style files. A file formatted like this:

name,artist,publisher,date,rpm, condition, sleeve
Wee small hours,Frank Sinatra,Columbia,1953,33,good,original

is easy to read like so:

import csv 
records = []
with open ('/path/to/file.csv', 'rb') as filehandle:
     reader = csv.DictReader(filehandle)
     for r in reader: 
         # make sure the dates are numeric so you can compare them
         try:
             r['date'] = int(r['date'])
         except:
             r['date'] = 1900 # you might prefer some other default for bad data
         records.append(r)

DictReader is especially nice since it automatically parses the first line as the headers and returns a dictionary so you don't need to create a custom class to organize your data (as an aside: you could also do this using the sqllite module, which would let you read and write the entire database directly and query it with SQL. I'm not going to go into that since SQL is it's own thing - but for bigger applications it would be the right thing to research). Writing out the data is the inverse of the above:

  # assume our records are dictionaries in the records variable...
  with open ('/path/to/file.csv', 'wb') as filehandle:
     writer = csv.DictWriter(filehandle, fieldnames = ('name','artist','publisher','date','rpm','condition','sleeve'))
     writer.writerows(records)

So, using CSV files and DictReader/DictWriter to handle the reading/writing, our 'database' is a list of dictionaries':

[
{'publisher': 'Columbia', 'name': 'Wee small hours', 'artist': 'Frank Sinatra', ' sleeve': 'original', 'date': '1953', 'rpm': '33', ' condition': 'good'}
{'publisher': ' Electra', 'name': 'Jump', 'artist': ' Van Halen', ' sleeve': ' repaired', 'date': ' 1983', 'rpm': ' 33', ' condition': ' poor'}
 # etc
]

Searching this can be done very efficiently using the built in filter function. Filter takes a list and a function as arguments; it returns a list of all the items where the function returns true. For example:

 sinatra = lambda p: 'sinatra' in p['artist'].lower()
 sinatra_albums = filter(records, sinatra)

 before_1970 = lambda p: p['date'] < 1970
 old_stuff = filter(records, before_1970)

and so on.

The 'ui' for the program then really amounts to creating custom filter functions and returning them. You can manufacture them easily:

 def make_artist_filter(artist):
     return lambda x: artist.lower() in x['artist']

 def make_year_filter(year):
     return lambda x: x['date'] == year

and so on. You can even combine them:

 def and_filter (filter1, filter22):
     return lambda x: filter1(x) and filter2(x)

 def or_filter (filter1, filter2):
     return lambda x: filter1(x) or filter2(x)

All of these filters automatically return lists, so you don't have to loop or create temporary variables to collect matches.

I think it's easy to extrapolate from this how to write the program very compactly; using standard library functions means you can concentrate on logic and user-facting stuff (do you want to include partial name matches, or regular expressions, for example? Do you silently ignore bad input or scold the user? etc).

\$\endgroup\$
  • \$\begingroup\$ This is incredible. I generally don't use csv's but you have convinced me \$\endgroup\$ – KnightOfNi Jan 21 '14 at 20:45
3
\$\begingroup\$

This can be simplified a bit further and made more efficient. You only need to open one of the text files (the one corresponding to the search term) to count the number of hits, and can then search through the others one at a time to get the full search results. Iterating through a file (e.g. for line_number, line in enumerate(file)) is probably better than using readlines here.

path = 'r"C:\Users\User\Desktop\RcrdCat\{}.txt'.format

# fields consist of an abbreviation, file name, and label
fields = (('name', 'names', 'Names'),
          ('authors', 'authors', 'Authors'),
          ('year', 'dates', 'Year'),
          ('pub', 'pubs', 'Publisher'),
          ('rpm', 'rpms', 'RPM'),
          ('cond', 'conditions', 'Condition'),
          ('sleeve', 'sleeves', 'Sleeve'))
field_dict = {field[0]: field[1] for field in fields}

def searchrecs(term):
    search_field, search_term = term
    try:
        file_name = field_dict[search_field]
    except KeyError:
        eg.msgbox("Please enter valid search criteria")
        return
    with open(path(file_name), 'r') as file:
        results = [{'line number': i, search_field: line} 
                   for i, line in enumerate(file) if search in line]
    eg.msgbox("Found {} hits. Click OK to view.".format(len(results)))
    for field, file_name, _ in fields:
        if field == search_field: # we've already searched this one
            continue
        with open(path(file_name), 'r') as file:
            numbered_file = enumerate(file)
            for result in results:
                for i, line in numbered_file:
                    if i == result['line number']:
                        result[field] = line
                        break
    text = ''
    for result in results:
        text += ' '.join('{}: {}'.format(label, result[field])
                         for field, _, label in fields) + '\n'
    eg.textbox('', 'Results', text)
\$\endgroup\$
  • \$\begingroup\$ This is an interesting approach which I like except that it opens and closes files each time it is run. Is it not possible that each file is only opened once? \$\endgroup\$ – James Khoury Jan 21 '14 at 3:29
  • \$\begingroup\$ yes it is possible as in your original code. You can append the opened file objects to the tuples in fields and just refer to the file in subsequent code rather than using with open... \$\endgroup\$ – Stuart Jan 21 '14 at 10:16
2
\$\begingroup\$

Here's an example of how to implement with sqlite3.

You'll need to import the following:

import sqlite3
from contextlib import contextmanager

And define the following constants:

DATABASE_PATH = '/tmp/my_database.db'
SCHEMA = '''\
DROP TABLE IF EXISTS records;
CREATE TABLE records (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    year INTEGER,
    name2 TEXT,
    publisher TEXT,
    rpm INTEGER,
    condition TEXT,
    sleeve INTEGER,
    artist TEXT
);''' # The schema script ONLY needs to be executed once, when first creating the database

Then add the following database utility functions to your script.

# Provides a database access context that ensures the database object is
# closed at the end of the transaction, no matter what.
@contextmanager
def get_db():
    try:
        db = sqlite3.connect(DATABASE_PATH)
        db.row_factory = sqlite3.Row
        yield db
    finally:
        db.close()

# Executed ONCE, to create the database.
# Do the following at the command line:
#   $ python -c "from import <my_record_program_name> import init_db; print(init_db())"
def init_db():
    try:
        with get_db() as db:
            db.executescript(SCHEMA);

        return "passed"

    except Exception as e:
        return "failed", e

Now modify add_recs():

def add_recs():

    # Your GUI logic here

    with get_db() as db:
        db.execute('''
            INSERT INTO 
            records (name, year, publisher, rpm, condition, name2, artist) 
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', 
            info)

        db.commit()

Finally, rewrite search_recs():

def search_recs(term):
    with get_db() as db:
        cur = db.execute(
            'SELECT * FROM records WHERE `%s`=?' % term[0], [term[1]])

        results = cur.fetchall()

    # Do GUI stuff with "results" here.
\$\endgroup\$
  • \$\begingroup\$ why sleeve INTEGER? Are you suggesting 1 for "yes" and 0 for "no"? And in case that was an exceptionally stupid question, I really don't use this module often... \$\endgroup\$ – KnightOfNi Jan 23 '14 at 14:57
  • \$\begingroup\$ SQLite, as the name implies, is an extremely light version of SQL, and as such there are only four different data types. Booleans are represented as integers. In any case, if you choose not to use a rdbms, I would recommend a) storing your records in one file, and b) loading them all into memory on program startup. Unless you have millions of records, your computer should have the memory to cope. \$\endgroup\$ – Joel Cornett Jan 23 '14 at 16:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.