Skip to main content

Ralsina.Me — Roberto Alsina's website

Posts about python (old posts, page 70)

PyQt Quickie: QTimer

QTimer is a fair­ly sim­ple class: you use it when you want some­thing to hap­pen "in a while" or "ev­ery once in a while".

The first case is some­thing like this:

# call f() in 3 seconds
QTimer.singleShot(3000, f)

The sec­ond is this:

# Create a QTimer
timer = QTimer()
# Connect it to f
timer.timeout.connect(f)
# Call f() every 5 seconds
timer.start(5000)

Sim­ple, right? Well, yes, but it has some trick­s.

  1. You have to keep a re­f­er­ence to``­­timer``

    If you don't, it will­get garbage-­­col­lec­t­ed, and f() will nev­er be called.

  2. It may not call f() in 5 sec­ond­s.

    It will call f() more or less 5 sec­onds af­ter you en­ter the event loop. That may not be quick­ly af­ter you start the timer at al­l!

  3. You may get over­lap­ping cal­l­s.

    If f() takes long to fin­ish and re-en­ters the event loop (for ex­am­ple, by call­ing pro­ces­sEv­ents) maybe the timer will time­out and call it again be­fore it's fin­ished. That's al­most nev­er a good thing.

So, you can do this:

def f():
    try:
        # Do things
    finally:
        QTimer.singleShot(5000, f)

f()

What that snippet does, is, calls f() only once. But f itself schedules itself to run in 5 seconds. Since it does it in a finally, it will do so even if things break.

That means no overlapping calls. It also means it won't be called every 5 seconds, but 5 seconds plus whatever f takes to run. Also, no need to keep any reference to a QTimer.

Fi­nal tip: You can al­so use QTimer to do some­thing "as soon as you are in the event loop"

QTimer.singleShot(0, f)

Hope it was use­ful!

Abandonment issues: rst2pdf

Of all the corpses of my pro­ject­s, there is one thatI feel worse about, which is rst2pdf. I feel bad about aband­non­ing sev­er­al, but rst2pdf was ac­tu­al­ly a use­ful tool, used by a bunch of peo­ple, and that it has nev­er gath­ered enough mo­men­tum with oth­er de­vel­op­ers is sad.

So, I will pick it up. I will spend about 4 hours a week on it. The plan is to:

  1. Gath­­er some patch­es that are lin­ger­ing on the is­­sue track­­er

  2. Fix some sim­­ple-ish bugs

  3. Make an­oth­er re­lease with 1) and 2)

And of course:

  1. Not let it fall in dis­­re­­pair again

In the meantime, here is a nice thing I just heard about. Dimitri Christodoulou has hacked rst2pdf so that it can handle the raw:: html directive.

This, dear friends is com­plete­ly nut­s, ab­so­lute­ly out of scope for any giv­en do­cu­tils tool, and just too cool :-)

I will try to hi­jack his code (prop­er cred­it and so on), and in­cor­po­rate it in­to rst2pdf.

And Dim­itri, or any­one else who wants to do cool stuff with rst2pdf: let me know! I will give you com­mit rights im­me­di­ate­ly!

Python context managers: they are easy!

This comes from this thread in the Python Ar­genti­na mail­ing list (which I strong­ly rec­om­mend if you read span­ish).

I was the oth­er day try­ing to do shel­l-scrip­t-­like-things on python (as part of a mon­ster set­up.py) and I was an­noyed that in shell it's easy to do this:

cd foo
bar -baz
cd -

Or this:

pushd foo
bar -baz
popd

Or this:

(cd foo && bar -baz)

And on Python I had to do this, which is ver­bose and ug­ly:

cwd = os.getcwd()
try:
    os.chdir('foo')
    os.system('bar -baz')
finally:
    os.chdir(cwd)

This is what I want­ed to have:

with os.chdir('foo'):
    os.system('bar -baz')

And of course, you can't do that. So, I asked, how do you im­ple­ment that con­text man­ager? I got sev­er­al an­swer­s.

  1. That's avail­able in Fab­ric:

    with cd("­foo"):
        run("bar")
  2. It's not hard to do:

    class DirCon­textM(ob­jec­t):
        def __init__(­self, new_dir):
            self­.new_dir = new_dir
            self­.old_dir = None
    
        def __en­ter__(­self):
            self­.old_dir = os­.getcwd()
            os­.chdir(­self.new_dir)
    
        def __ex­it__(­self, *_):
            os­.chdir(­self.old_dir)
  3. It's even eas­i­er to do:

    from con­textlib im­port con­textman­ag­er
    
    @con­textman­ag­er
    def cd(­path):
        old_dir = os­.getcwd()
        os­.chdir(­path)
        yield
        os­.chdir(old_dir)
  4. That's cool, so let's add it to path.py

  5. Maybe check for ex­­cep­­tions

    @con­textman­ag­er
    def cd(­path):
        old_dir = os­.getcwd()
        os­.chdir(­path)
        try:
            yield
        fi­nal­ly:
            os­.chdir(old_dir)

All in al­l, I learned how to do con­text man­ager­s, about con­textlib, about fab­ric and about path.py. Which is not bad for 15 min­utes :-)

Shipping your PyQt app for windows

I have writ­ten about this in the past, with the gen­er­al con­clu­sion be­ing "it's a pain in the as­s".

So, now, here is how it's done.

  1. Start with a work­ing PyQt ap­­pli­­ca­­tion. In this ex­am­­ple, I will use de­vi­­cen­­zo.py most­­ly be­­cause:

    1. It is a work­ing PyQt ap­­­pli­­­ca­­­tion.

    2. It us­es a big chunk of PyQt

    3. It's easy to test

  2. Now you need a set­up.py. Here's one that work­s, with ex­ten­sive com­m­ments.

# We will be using py2exe to build the binaries.
# You may use other tools, but I know this one.

from distutils.core import setup
import py2exe

# Now you need to pass arguments to setup
# windows is a list of scripts that have their own UI and
# thus don't need to run in a console.

setup(windows=['devicenzo.py'],
      options={

# And now, configure py2exe by passing more options;

          'py2exe': {

# This is magic: if you don't add these, your .exe may
# or may not work on older/newer versions of windows.

              "dll_excludes": [
                  "MSVCP90.dll",
                  "MSWSOCK.dll",
                  "mswsock.dll",
                  "powrprof.dll",
                  ],

# Py2exe will not figure out that you need these on its own.
# You may need one, the other, or both.

              'includes': [
                  'sip',
                  'PyQt4.QtNetwork',
                  ],

# Optional: make one big exe with everything in it, or
# a folder with many things in it. Your choice
#             'bundle_files': 1,
          }
      },

# Qt's dynamically loaded plugins and py2exe really don't
# get along.

data_files = [
            ('phonon_backend', [
                'C:\Python27\Lib\site-packages\PyQt4\plugins\phonon_backend\phonon_ds94.dll'
                ]),
            ('imageplugins', [
            'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qgif4.dll',
            'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qjpeg4.dll',
            'c:\Python27\lib\site-packages\PyQt4\plugins\imageformats\qsvg4.dll',
            ]),
],

# If you choose the bundle above, you may want to use this, too.
#     zipfile=None,
)
  1. Run python set­up.py py2exe and get a dist fold­er full of bi­na­ry good­ness.

And that's it. Ex­cept of course, that's not it.

What this will do is cre­ate a bi­na­ry set, ei­ther a fold­er full of things, or a sin­gle EXE file. And that's not enough. You have to con­sid­er at least the fol­low­ing:

  1. Put ev­ery­thing in re­­source files: im­ages, qss files, icon­s, etc. Ev­ery file your app need­s? Put it in a re­­source file and load it from there. That way you don't have to care about them if you go the "one ex­e" road.

  2. Com­pile .ui files to .py (same rea­­son)

  3. Fig­ure out if you use Qt's plu­g­in­s, and make them work. This in­­­cludes: us­ing Phonon, us­ing Qt­SQL, and us­ing any im­age for­­mats oth­­er than PNG.

Af­ter you have that, are you done? NO!

Your win­dows us­er will want an in­stall­er. I am not go­ing to go in­to de­tail­s, but I had a good time us­ing Bi­tRock­'s In­stall­Builder for Qt. It's a nice tool, and it work­s. That's a lot in this field.

But is that al­l? NO!

You have to take care of the Vis­ual Stu­dio Run­time. My sug­ges­tion? Get a copy of the 1.1MB vcre­dis­t_x86.exe (not the larg­er one, the 1.1MB one), and ei­ther tell peo­ple to in­stall it man­u­al­ly, or add it to your in­stall­er. You are legal­ly al­lowed (AFAIK) to re­dis­tribute that thing as a whole. But not what's in it (un­less you have a VS li­cense).

And we are done? NO!

Once you run your app "in­stalled", if it ev­er prints any­thing to stder­r, you will get ei­ther a di­a­log telling you it did, or worse (if you are in ay­thing new­er than XP), a di­a­log telling you it can't write to a log file, and the app will nev­er work again.

This is be­cause py2exe catch­es stderr and tries to save it on a log­file. Which it tries to cre­ate in the same fold­er as the bi­na­ry. Which is usu­al­ly not al­lowed be­cause of per­mis­sion­s.

So­lu­tion? Your app should nev­er write to stder­r. Write an ex­cepthook and catch that. And then re­move stderr or re­place it with a log file, or some­thing. Just don't let py2exe do it, be­cause the way py2exe does it is bro­ken.

And is that it?

Well, ba­si­cal­ly yes. Of course you should get 4 or 5 dif­fer­ent ver­sions of win­dows to test it on, but you are pret­ty much free to ship your app as you wish. Oh, mind you, don't up­load it to down­load­s.­com be­cause they will wrap your in­stall­er in a larg­er one that in­stalls bloat­ware and crap.

So, there you go.

Creating a forum the easy way (32 LOC)

This is on­ly the first part of a project to cre­ate the sim­plest (for me) soft­ware fo­rum pos­si­ble.

Here are the fea­tures I wan­t:

  • Lo­­gin us­ing twit­ter / Face­­book / Google / OpenID

  • Un­lim­it­ed num­ber of threads

  • Sup­­port for like / dis­­­like both on threads and on posts

  • Avatars

  • HTML in com­­ments

  • Mail the us­er on replies

  • RSS feeds for threads

You can see it in ac­tion at http://­foro.net­man­ager­s.­com.ar (for a lim­it­ed time on­ly ;-)

And here is the code:

import bottle
import disqusapi as disqus
import json
shortname = 'magicmisteryforum'
api = disqus.DisqusAPI(open("key").read().strip())

@bottle.route('/', method='GET')
def index():
    msg = bottle.request.GET.get('msg', '')
    threads = api.forums.listThreads(forum=shortname, limit=100)
    print threads[0]
    return bottle.template('main.tpl', threads=threads, shortname=shortname, msg=msg)

@bottle.route('/new', method='POST')
def new():
    title = bottle.request.forms.get('title', None)
    if not title:
        bottle.redirect('/?msg=Missing%20Thread%20Name')
        return
    thread = api.threads.create(forum=shortname, title = title)
    thread_id = thread.__dict__['response']['id']
    # Redirecting to /thread/thread_id doesn't work
    # because threads take a few seconds to appear on the listing
    bottle.redirect('/')

@bottle.route('/thread/:id')
def thread(id):
    t = api.threads.details(thread=id)
    return bottle.template('thread.tpl', shortname=shortname, id=id, thread=t.__dict__['response'])

@bottle.route('/static/:path#.+#')
def server_static(path):
    return bottle.static_file(path, root='./static')

app = bottle.app()
app.catchall = False #Now most exceptions are re-raised within bottle.
bottle.run(host='184.82.108.14', port=80, app=app)

It re­quires Bot­tle and the Dis­qus python API

Of course, there is al­so a bit of tem­plat­ing in­volved, here is main.t­pl and the thread­.t­pl. Since I suck at HTM­L, it us­es Bluetrip CSS and it's more than sim­ple enough to cus­tom­ize.

OF COURSE I AM CHEAT­ING!

This thing is just a sim­ple ve­neer around Dis­qus! More like a blog with com­ments and with­out posts than a fo­rum! But ... what's miss­ing to make this a re­al fo­rum? It work­s, does­n't it? You could even use Dis­qus cat­e­gories to cre­ate sub­fo­rum­s...

All things con­sid­ered, I think it's a cute hack.

And if you wait a few days, this will lead to some­thing much more mag­i­cal!

Full source code at http://­mag­ic­fo­rum.­google­code.­com


Contents © 2000-2020 Roberto Alsina