2011-07-24 21:20

Shipping your PyQt app for windows

I have written about this in the past, with the general conclusion being "it's a pain in the ass".

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

  1. Start with a working PyQt application. In this example, I will use devicenzo.py mostly because:
    1. It is a working PyQt application.
    2. It uses a big chunk of PyQt
    3. It's easy to test
  2. Now you need a setup.py. Here's one that works, with extensive commments.
# 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 setup.py py2exe and get a dist folder full of binary goodness.

And that's it. Except of course, that's not it.

What this will do is create a binary set, either a folder full of things, or a single EXE file. And that's not enough. You have to consider at least the following:

  1. Put everything in resource files: images, qss files, icons, etc. Every file your app needs? Put it in a resource file and load it from there. That way you don't have to care about them if you go the "one exe" road.
  2. Compile .ui files to .py (same reason)
  3. Figure out if you use Qt's plugins, and make them work. This includes: using Phonon, using QtSQL, and using any image formats other than PNG.

After you have that, are you done? NO!

Your windows user will want an installer. I am not going to go into details, but I had a good time using BitRock's InstallBuilder for Qt. It's a nice tool, and it works. That's a lot in this field.

But is that all? NO!

You have to take care of the Visual Studio Runtime. My suggestion? Get a copy of the 1.1MB vcredist_x86.exe (not the larger one, the 1.1MB one), and either tell people to install it manually, or add it to your installer. You are legally allowed (AFAIK) to redistribute that thing as a whole. But not what's in it (unless you have a VS license).

And we are done? NO!

Once you run your app "installed", if it ever prints anything to stderr, you will get either a dialog telling you it did, or worse (if you are in aything newer than XP), a dialog telling you it can't write to a log file, and the app will never work again.

This is because py2exe catches stderr and tries to save it on a logfile. Which it tries to create in the same folder as the binary. Which is usually not allowed because of permissions.

Solution? Your app should never write to stderr. Write an excepthook and catch that. And then remove stderr or replace it with a log file, or something. Just don't let py2exe do it, because the way py2exe does it is broken.

And is that it?

Well, basically yes. Of course you should get 4 or 5 different versions of windows to test it on, but you are pretty much free to ship your app as you wish. Oh, mind you, don't upload it to downloads.com because they will wrap your installer in a larger one that installs bloatware and crap.

So, there you go.

2011-07-23 22:57

As I was saying yesterday...

I have not posted in this blog in a long time.One ofthe reasons is that a whole lot of things have happened in my life, and I have not blogged about them.

It has gotten so that if I were to "catchup" here, I would need to post a 35-page post with 300 pictures, 100 code snippets, 50 rants and maybe 3 youtube videos.

So... screw that and let's pretend that was all a dream and that you (if there are still any "you" out there) have not been neglected, and all is good.

So, what am I posting about? Oh, just a short rant.

I am saddened and pissed off by people who proudly say "I don't know anything about X", where X is math, knitting, astrophysics, football, medicine, pet care, hairdressing, coffee brewing, politics, or dolphin-rearing (this doesn't apply to wine. It's perfectly ok not to care about wine, because most people who do care about wine are faking it).

There are tons of things I know nothing about. And you know what? Each one is a personal failure. Every time I see my mom knitting, and I simply cannot grasp how the hell it works, it pisses me off.

While it looks mind-crushingly boring, she's basically taking a string, make some knots using sticks, and producing a freaking sweater. How cool is that? I say pretty damn cool.

Whenever I see something that looks boring, or uninteresting, I always think, there is, somewhere, a person who has spent years of his life being the best at that. There is, somewhere, a person which is the world's authority on how to tie a string to make a sweater.

There is someone who is the non-plus-ultra of laying out coloured tiles so it looks good in the shape of a dolphin.

There is someone who is the best at using an incredibly inaccurate spraycan to draw on a wall.

There is someone who can look at a lot full of cars and tell you the logical order to remove them so they leave in minimum time.

There is someone who can hit a small ball, flying at 150km/h with a round stick, 1 out of every 3 times.

There is someone who can guess what word goes with almost every short definition.

There is someone who can tell you all regular spanish verbs that start with T.

There is someone who knows all about the history of journalism in Turkey (I actually walked in front of the Museum of Turkish Journalism, and it pains me that I couldn't go at the time).

There is someone who knows the dialog for every simpsons episode.

Every one of those things, and billions more, may be boring to me, but that is surely because I am failing at noticing the fun in it, it's a roughness of my soul, a failure of my perception, a flaw in my vision, which cripples me into saying they are all boring.

Boredom is not a sign of superiority, it's a sign of inferiority. Next time you see a bored person, someone that has nothing he wants to do, who looks condescendingly at those who are passionate about something, no matter what that may be, feel sad for him.

2011-06-26 00:00

The Scar (New Crobuzon, #2)

  • Author: China Miéville
  • Rating:
  • See in goodreads
  • Review:

    I didn't know there were other New Crobuzon books other than Perdido Street Station, which I really enjoyed, so this was a very pleasant surprise.

    I didn't like it *quite* as much as Perdido Street Station, but that's just because I liked that other one so much.

    Gripping world building, nice plot, too. The characters are maybe a little too on the "misterious stranger" style, but hey, it's China Mieville so that was to be expected.

2011-03-25 02:30

Creating a forum the easy way (32 LOC)

This is only the first part of a project to create the simplest (for me) software forum possible.

Here are the features I want:

  • Login using twitter / Facebook / Google / OpenID
  • Unlimited number of threads
  • Support for like / dislike both on threads and on posts
  • Avatars
  • HTML in comments
  • Mail the user on replies
  • RSS feeds for threads

You can see it in action at http://foro.netmanagers.com.ar (for a limited time only ;-)

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 requires Bottle and the Disqus python API

Of course, there is also a bit of templating involved, here is main.tpl and the thread.tpl. Since I suck at HTML, it uses Bluetrip CSS and it's more than simple enough to customize.

OF COURSE I AM CHEATING!

This thing is just a simple veneer around Disqus! More like a blog with comments and without posts than a forum! But ... what's missing to make this a real forum? It works, doesn't it? You could even use Disqus categories to create subforums...

All things considered, I think it's a cute hack.

And if you wait a few days, this will lead to something much more magical!

Full source code at http://magicforum.googlecode.com

2011-03-11 19:37

About Japan and God and Lilita

As everyone knows, there was a big quake in Japan, then a Tsunami, then a volcano erupted, then a nuclear plant caught fire. All things considered, a really crappy week.

Then again, if I were japanese and I had to read idiots telling me this was because god is pubishing me because of (whatever the idiot doesn't like about Japan), I would be sorely tempted to find the morons and ... ok, considering the japanese are showing they are very reasonable people, probably just tell him something politely.

OTOH, I am not japanese. Which means I can explain in great detail why those who say "maybe it's <whatever> punishing | telling japan <something>" are a complete waste of oxygen.

I will focus on one example, because it's a very special religious moron: a presidential candidate in Argentina, called Lilita Carrió.

Here's what she said (spanish is the original, of course):

"Dios nos está diciendo que debemos cuidar el planeta, que no sigamos destruyendo la tierra, que vivamos en la verdad, en la decencia, en la justicia, que no usemos la tecnología, aunque sea de manera pacífica. Hay que leer los signos de los tiempos"

"God is telling us that we should take care of the planet, that we should stop destroying the earth, that we should live in truth, in decency, in justice, and stop using technology, even if it's peacefully. We should read the sign of the times".

Let's consider that little by little.

"God is telling us that we should take care of the planet"

I must confess I am amazed that an almightly being is less capable of communicating ideas than my 3.9 year old kid. When he wants me to play ball, he brings the ball and tells me "Dad, let's play ball".

On the other hand, god apparently, to tell us to stop using technology, causes a series of catastrophic events in the other end of the world, then brings us the news over the Internet (a technological miracle), so that Lilita can divine god's intentions and then re-broadcast them to us over the radio (of course, an erarlier technological miracle).

Now, does that make sense to anyone? I mean, why doesn't god just, you know, say what he means in a reasonable manner? Because for religious people, the fun is in the divination. They are acting like roman priests divining the future in the entrails of an animal, except they are using the life and suffering of people.

Oh, look, suffering in Japan, that means we should stop using the Wii!

Not only is that approach completely against everything christian doctrine teaches, from with the virtue of charity (if god did it to tell us something, by definition they deserved it!) to the injunction against divining god's messages in portents (yes, it is forbidden, go ask a priest).

"[God is telling us] that we should stop destroying the earth"

Oh, gee, ok then! OTOH, maybe a more subtle way than half-breaking everything in a whole country to let us know next time? Please?

"[God is telling us] that we should live in truth"

Ok, yes, let's do that. I will start by not believing in god, who truly does not exist. When you catch up to that we'll argue some more, ok?

"[God is telling us to] stop using technology, even if it's peacefully."

I would love if this presidential candidate didn't use technology because it would mean I would not have to see her sanctimonious stupidity ever again. OTOH, if we wouldn't have technology, we would probably not know about the earthquake yet. I suppose she may have been saying "nuclear technology" and this is out of context.

OTOH, number of people killed by peaceful nuclear technology since 1950: 1000? 10000?

number of people killed by earthquakes and tsunami in the last 5 years: 100000? 200000?

Yes, those are numbers I just made up, but I am betting they are more right than wrong, so, basically, god has killed more people this week telling us not to use nuclear power, than nuclear power has killed in the last 50 years. Not exactly good communication skills.

"We should read the sign of the times"

Ok, here it is:

The New York Times

Don't vote for this blithering idiot. She's dangerous, and probably mentally ill.

2011-03-11 01:47

New golfing challenge: PatoCabrera

In the spirit of the De Vicenzo web browser, I am starting a new program, called Pato Cabrera. Here are the rules:

  • Twitter client (no identi.ca in the first version, but to be added later)
  • Has these features: http://pastebin.lugmen.org.ar/6464
  • Has to be implemented before April 4th
  • Smaller than 16384 bytes (of python code) but may be larger because of artwork.

Let's see how it works :-)

2011-03-08 21:45

OK, so THAT is how much browser I can put in 128 lines of code.

I have already posted a couple of times (1, 2) about De Vicenzo , an attempt to implement the rest of the browser, starting with PyQt's WebKit... limiting myself to 128 lines of code.

Of course I could do more, but I have my standards!

  • No using ;
  • No if whatever: f()

Other than that, I did a lot of dirty tricks, but right now, it's a fairly complete browser, and it has 127 lines of code (according to sloccount) so that's enough playing and it's time to go back to real work.

But first, let's consider how some features were implemented (I'll wrap the lines so they page stays reasonably narrow), and also look at the "normal" versions of the same (the "normal" code is not tested, please tell me if it's broken ;-).

This is not something you should learn how to do. In fact, this is almost a treatise on how not to do things. This is some of the least pythonic, less clear code you will see this week.

It is short, and it is expressive. But it is ugly.

I'll discuss this version.

Proxy Support

A browser is not much of a browser if you can't use it without a proxy, but luckily Qt's network stack has good proxy support. The trick was configuring it.

De Vicenzo supports HTTP and SOCKS proxies by parsing a http_proxy environment variable and setting Qt's application-wide proxy:

 proxy_url = QtCore.QUrl(os.environ.get('http_proxy', ''))
 QtNetwork.QNetworkProxy.setApplicationProxy(QtNetwork.QNetworkProxy(\
 QtNetwork.QNetworkProxy.HttpProxy if unicode(proxy_url.scheme()).startswith('http')\
 else QtNetwork.QNetworkProxy.Socks5Proxy, proxy_url.host(),\
 proxy_url.port(), proxy_url.userName(), proxy_url.password())) if\
'http_proxy' in os.environ else None

How would that look in normal code?

if 'http_proxy' in os.environ:
    proxy_url = QtCore.QUrl(os.environ['http_proxy'])
    if unicode(proxy_url.scheme()).starstswith('http'):
        protocol = QtNetwork.QNetworkProxy.HttpProxy
    else:
        protocol = QtNetwork.QNetworkProxy.Socks5Proxy
    QtNetwork.QNetworkProxy.setApplicationProxy(
        QtNetwork.QNetworkProxy(
            protocol,
            proxy_url.host(),
            proxy_url.port(),
            proxy_url.userName(),
            proxy_url.password()))

As you can see, the main abuses against python here are the use of the ternary operator as a one-line if (and nesting it), and line length.

Persistent Cookies

You really need this, since you want to stay logged into your sites between sessions. For this, first I needed to write some persistence mechanism, and then save/restore the cookies there.

Here's how the persistence is done (settings is a global QSettings instance):

def put(self, key, value):
    "Persist an object somewhere under a given key"
    settings.setValue(key, json.dumps(value))
    settings.sync()

def get(self, key, default=None):
    "Get the object stored under 'key' in persistent storage, or the default value"
    v = settings.value(key)
    return json.loads(unicode(v.toString())) if v.isValid() else default

It's not terribly weird code, except for the use of the ternary operator in the last line. The use of json ensures that as long as reasonable things are persisted, you will get them with the same type as you put them without needing to convert them or call special methods.

So, how do you save/restore the cookies? First, you need to access the cookie jar. I couldn't find whether there is a global one, or a per-webview one, so I created a QNetworkCookieJar in line 24 and assign it to each web page in line 107.

# Save the cookies, in the window's closeEvent
self.put("cookiejar", [str(c.toRawForm()) for c in self.cookies.allCookies()])

# Restore the cookies, in the window's __init__
self.cookies.setAllCookies([QtNetwork.QNetworkCookie.parseCookies(c)[0]\
for c in self.get("cookiejar", [])])

Here I confess I am guilty of using list comprehensions when a for loop would have been the correct thing.

I use the same trick when restoring the open tabs, with the added misfeature of using a list comprehension and throwing away the result:

# get("tabs") is a list of URLs
[self.addTab(QtCore.QUrl(u)) for u in self.get("tabs", [])]

Using Properties and Signals in Object Creation

This is a feature of recent PyQt versions: if you pass property names as keyword arguments when you create an object, they are assigned the value. If you pass a signal as a keyword argument, they are connected to the given value.

This is a really great feature that helps you create clear, local code, and it's a great thing to have. But if you are writing evil code... well, you can go to hell on a handbasket using it.

This is all over the place in De Vicenzo, and here's one example (yes, this is one line):

QtWebKit.QWebView.__init__(self, loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None, loadFinished=self.pbar.hide, loadStarted=lambda:\
self.pbar.show() if self.amCurrent() else None, titleChanged=lambda\
t: container.tabs.setTabText(container.tabs.indexOf(self), t) or\
(container.setWindowTitle(t) if self.amCurrent() else None))

Oh, boy, where do I start with this one.

There are lambda expressions used to define the callbacks in-place instead of just connecting to a real function or method.

There are lambdas that contain the ternary operator:

loadStarted=lambda:\
    self.pbar.show() if self.amCurrent() else None

There are lambdas that use or or a tuple to trick python into doing two things in a single lambda!

loadProgress=lambda v:\
(self.pbar.show(), self.pbar.setValue(v)) if self.amCurrent() else\
None

I won't even try to untangle this for educational purposes, but let's just say that line contains what should be replaced by 3 methods, and should be spread over 6 lines or more.

Download Manager

Ok, calling it a manager is overreaching, since you can't stop them once they start, but hey, it lets you download things and keep on browsing, and reports the progress!

First, on line 16 I created a bars dictionary for general bookkeeping of the downloads.

Then, I needed to delegate the unsupported content to the right method, and that's done in lines 108 and 109

What that does is basically that whenever you click on something WebKit can't handle, the method fetch will be called and passed the network request.

def fetch(self, reply):
    destination = QtGui.QFileDialog.getSaveFileName(self, \
        "Save File", os.path.expanduser(os.path.join('~',\
            unicode(reply.url().path()).split('/')[-1])))
    if destination:
        bar = QtGui.QProgressBar(format='%p% - ' +
            os.path.basename(unicode(destination)))
        self.statusBar().addPermanentWidget(bar)
        reply.downloadProgress.connect(self.progress)
        reply.finished.connect(self.finished)
        self.bars[unicode(reply.url().toString())] = [bar, reply,\
            unicode(destination)]

No real code golfing here, except for long lines, but once you break them reasonably, this is pretty much the obvious way to do it:

  • Ask for a filename
  • Create a progressbar, put it in the statusbar, and connect it to the download's progress signals.

Then, of course, we need ths progress slot, that updates the progressbar:

progress = lambda self, received, total:\
    self.bars[unicode(self.sender().url().toString())][0]\
    .setValue(100. * received / total)

Yes, I defined a method as a lambda to save 1 line. [facepalm]

And the finished slot for when the download is done:

def finished(self):
    reply = self.sender()
    url = unicode(reply.url().toString())
    bar, _, fname = self.bars[url]
    redirURL = unicode(reply.attribute(QtNetwork.QNetworkRequest.\
        RedirectionTargetAttribute).toString())
    del self.bars[url]
    bar.deleteLater()
    if redirURL and redirURL != url:
        return self.fetch(redirURL, fname)
    with open(fname, 'wb') as f:
        f.write(str(reply.readAll()))

Notice that it even handles redirections sanely! Beyond that, it just hides the progress bar, saves the data, end of story. The longest line is not even my fault!

There is a big inefficiency in that the whole file is kept in memory until the end. If you download a DVD image, that's gonna sting.

Also, using with saves a line and doesn't leak a file handle, compared to the alternatives.

Printing

Again Qt saved me, because doing this manually would have been a pain. However, it turns out that printing is just ... there? Qt, specially when used via PyQt is such an awesomely rich environment.

self.previewer = QtGui.QPrintPreviewDialog(\
    paintRequested=self.print_)
self.do_print = QtGui.QShortcut("Ctrl+p",\
    self, activated=self.previewer.exec_)

There's not even any need to golf here, that's exactly as much code as you need to hook Ctrl+p to make a QWebView print.

Other Tricks

There are no other tricks. All that's left is creating widgets, connecting things to one another, and enjoying the awesome experience of programming PyQt, where you can write a whole web browser (except the engine) in 127 lines of code.

2011-03-05 23:46

De Vicenzo: A much cooler mini web browser.

It seems it was only a few days ago that I started this project. Oh, wait, yes, it was just a few days ago!

If you don't want to read that again, the idea is to see just how much code is needed to turn Qt's WebKit engine into a fully-fledged browser.

To do that, I set myself a completely arbitrary limit: 128 lines of code.

So, as of now, I declare it feature-complete.

The new features are:

  • Tabbed browsing (you can add/remove tabs)
  • Bookmarks (you can add/remove them, and choose them from a drop-down menu)

This is what already worked:

  • Zoom in (Ctrl++)
  • Zoom out (Ctrl+-)
  • Reset Zoom (Ctrl+=)
  • Find (Ctrl+F)
  • Hide find (Esc)
  • Buttons for back/forward and reload
  • URL entry that matches the page + autocomplete from history + smart entry (adds http://, that kind of thing)
  • Plugins support (including flash)
  • The window title shows the page title (without browser advertising ;-)
  • Progress bar for page loading
  • Statusbar that shows hovered links URL
  • Takes a URL on the command line, or opens http://python.org
  • Multiplatform (works in any place QtWebKit works)

So... how much code was needed for this? 87 LINES OF CODE

Or if you want the PEP8-compliant version, 115 LINES OF CODE.

Before anyone says it: yes, I know the rendering engine and the toolkit are huge. What I wrote is just the chrome around them, just like Arora, Rekonq, Galeon, Epiphany and a bunch of others do.

It's simple, minimalistic chrome, but it works pretty good, IMVHO.

Here it is in (buggy) action:

It's more or less feature-complete for what I expected to be achievable, but it still needs some fixes.

You can see the code at it's own home page: http://devicenzo.googlecode.com

2011-02-28 21:10

How much web browser can you put in 128 lines of code?

UPDATE: If you read this and all you can say is "oh, he's just embedding WebKit", I have two things to tell you:

  1. Duh! Of course the 128 lines don't include the rendering engine, or the TCP implementation, or the GUI toolkit. This is about the rest of the browser, the part around the web rendering engine. You know, just like Arora, Rekonq, Epiphany, and everyone else that embeds webkit or mozilla does it? If you didn't get that before this explanation... facepalm.
  2. Get your favourite webkit fork and try to do this much with the same amount of code. I dare you! I double dog dare you!

Now back to the original article


Today, because of a IRC chat, I tried to find a 42-line web browser I had written a while ago. Sadly, the pastebin where I posted it was dead, so I learned a lesson: It's not a good idea to trust a pastebin as code repository

What I liked about that 42-line browser was that it was not the typical example, where someone dumps a Webkit view in a window, loads a page and tries to convince you he's cool. That one is only 7 lines of code:

import sys
from PyQt4 import QtGui,QtCore,QtWebKit
app=QtGui.QApplication(sys.argv)
wb=QtWebKit.QWebView()
wb.setUrl(QtCore.QUrl('http://www.python.org'))
wb.show()
sys.exit(app.exec_())

And if I wanted to make the code uglier, it could be done in 6.

But anyway, that 42-line browser actually looked useful!

This 42-line web browser, courtesy of #python and #qt -- http... on Twitpic

Those buttons you see actually worked correctly, enabling and disabling at the right moment, the URL entry changed when you clicked on links, and some other bits.

So, I have decided to start a small, intermittent project of code golf: put as much browser as I can in 128 lines of code (not counting comments or blanks), starting with PyQt4.

This has a useful purpose: I always suspected that if you assumed PyQt was part of the base system, most apps would fit in floppies again. This one fits on a 1.44MB floppy some 500 times (so you could use 360KB commodore floppies if you prefer!).

So far, I am at about 50 lines, and it has the following features:

  • Zoom in (Ctrl++)
  • Zoom out (Ctrl+-)
  • Reset Zoom (Ctrl+=)
  • Find (Ctrl+F)
  • Hide find (Esc)
  • Buttons for back/forward and reload
  • URL entry that matches the page + autocomplete from history + smart entry (adds http://, that kind of thing)
  • Plugins support (including flash)
  • The window title shows the page title (without browser advertising ;-)
  • Progress bar for page loading
  • Statusbar that shows hovered links URL
  • Takes a URL on the command line, or opens http://python.org
  • Multiplatform (works in any place QtWebKit works)

Missing are tabs and proxy support. I expect those will take another 40 lines or so, but I think it's probably the most featureful of these toy browsers.

The code... it's not all that hard. I am using lambda a lot, and I am using PyQt's keyword arguments for signal connection which makes lines long, but not hard. It could be made much smaller!

Here it is in action:

And here's the code:

#!/usr/bin/env python
"A web browser that will never exceed 128 lines of code. (not counting blanks)"

import sys
from PyQt4 import QtGui,QtCore,QtWebKit

class MainWindow(QtGui.QMainWindow):
    def __init__(self, url):
        QtGui.QMainWindow.__init__(self)
        self.sb=self.statusBar()

        self.pbar = QtGui.QProgressBar()
        self.pbar.setMaximumWidth(120)
        self.wb=QtWebKit.QWebView(loadProgress = self.pbar.setValue, loadFinished = self.pbar.hide, loadStarted = self.pbar.show, titleChanged = self.setWindowTitle)
        self.setCentralWidget(self.wb)

        self.tb=self.addToolBar("Main Toolbar")
        for a in (QtWebKit.QWebPage.Back, QtWebKit.QWebPage.Forward, QtWebKit.QWebPage.Reload):
            self.tb.addAction(self.wb.pageAction(a))

        self.url = QtGui.QLineEdit(returnPressed = lambda:self.wb.setUrl(QtCore.QUrl.fromUserInput(self.url.text())))
        self.tb.addWidget(self.url)

        self.wb.urlChanged.connect(lambda u: self.url.setText(u.toString()))
        self.wb.urlChanged.connect(lambda: self.url.setCompleter(QtGui.QCompleter(QtCore.QStringList([QtCore.QString(i.url().toString()) for i in self.wb.history().items()]), caseSensitivity = QtCore.Qt.CaseInsensitive)))

        self.wb.statusBarMessage.connect(self.sb.showMessage)
        self.wb.page().linkHovered.connect(lambda l: self.sb.showMessage(l, 3000))

        self.search = QtGui.QLineEdit(returnPressed = lambda: self.wb.findText(self.search.text()))
        self.search.hide()
        self.showSearch = QtGui.QShortcut("Ctrl+F", self, activated = lambda: (self.search.show() , self.search.setFocus()))
        self.hideSearch = QtGui.QShortcut("Esc", self, activated = lambda: (self.search.hide(), self.wb.setFocus()))

        self.quit = QtGui.QShortcut("Ctrl+Q", self, activated = self.close)
        self.zoomIn = QtGui.QShortcut("Ctrl++", self, activated = lambda: self.wb.setZoomFactor(self.wb.zoomFactor()+.2))
        self.zoomOut = QtGui.QShortcut("Ctrl+-", self, activated = lambda: self.wb.setZoomFactor(self.wb.zoomFactor()-.2))
        self.zoomOne = QtGui.QShortcut("Ctrl+=", self, activated = lambda: self.wb.setZoomFactor(1))
        self.wb.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True)

        self.sb.addPermanentWidget(self.search)
        self.sb.addPermanentWidget(self.pbar)
        self.wb.load(url)


if __name__ == "__main__":
    app=QtGui.QApplication(sys.argv)
    if len(sys.argv) > 1:
        url = QtCore.QUrl.fromUserInput(sys.argv[1])
    else:
        url = QtCore.QUrl('http://www.python.org')
    wb=MainWindow(url)
    wb.show()
    sys.exit(app.exec_())

2011-02-22 22:45

The Outhouse and the Mall

Wearing the software engineer's hat: Code is the most trivial and the least important part of a feature.

—Michael Iatrou

Michael tweeted that, I replied, he replied, but what the heck, I think sometimes things can be better explained in more than 140 characters, thus this post [1].

So, why the mall and the outhouse? Because when we talk about software and code and features, we are not all talking about the same thing. Imagine if I told you that bricks are trivial. After all, they have existed in their current form for thousands of years, they are pretty simple to manufacture, and have no interesting features, really, except a certain resistence.

Now, suppose you are building an outhouse. Since you are a funny guy, you want to build an actual brick outhouse so you will use bricks to do it.

Now, since bricks are so boring, you may feel compelled to believe bricks are the least important part of your edifice, and that the overall design is more important. Should you carve a moon-shaped hole in the door? How deep should the latrine be?

However, that position is fatally flawed, since if you ignore those trivial, boring bricks, all you have is shit in a hole in the ground. That is because you are considering the bricks as just a mean to your end. You only care about the bricks insofar as they help you realize your grand outhouse vision. I am here to tell you that you are wrong.

The first way in which you are wrong is in that artificial separation between means and ends. Everyone is familiar with the ethical conundrum about whether the ends justify the means, but that's garbage. That'swhat you say when you try to convince yourself that doing things haphazardly is ok, because what you do is just the means to whatever other thing is the end. Life is not so easily divided into things that matter and things that don't.

Your work, your creation is not just some ideal isolated end towards which you travel across a sea of dirty means, trying to keep your silver armour clean. It's one whole thing. You are creating the means, you are creating your goal, you are responsible for both, and if you use shoddy bricks, your outhouse should shame you.

In the same way, if you do crappy code, your feature is demeaned. It may even work, but you will know it's built out of crap. You will know you will have to fix and maintain that crap for years to come, or, if you are lucky, ruin your karma by dumping it on the head of some poor sucker who follows your steps.

I am pretty much a materialist. If you remove the code, you don't have a feature, or software, you have a concept, maybe an idea, perhaps a design (or maybe not) but certainly not software, just like you don't have a brick outhouse without piling some damn bricks one on top of the other.

I always say, when I see someone calling himself a software engineer, that I am merely a software carpenter. I know my tools, I care about them, I use them as well as I can according to my lights [2] and I try to produce as good a piece of furniture as I can with what I am given.

This tends to produce humble software, but it's software that has one redeeming feature: it knows what it should do, and does it as well as I can make it. For example, I wrote rst2pdf. It's a program that takes some sort of text, and produces PDF files. It does that as well as I could manage. It does nothing else. It works well or not, but it is what it is, it has a purpose, a description and a goal, and I have tried to achieve that goal without embarrasing myself.

My programs are outhouses, made of carefully selected and considered bricks. They are not fancy, but they are what they are and you know it just by looking at them. And if you ever need an outhouse, well, an outhouse is what you should get.

Also, people tend to do weird stuff with them I never expected, but that's just the luck of the analogy.

But why did I mention malls in the title? Because malls are not outhouses. Malls are not done with a goal by themselves beyond making money for its builders. The actual function of a piece of mall is not even known when it's being built. Will this be a McDonalds, or will it be a comic book store? Who knows!

A mall is built quickly with whatever makes sense moneywise, and it should look bland and recognisable, to not scare the herd. It's a building made for pedestrians, but it's intended to confuse them and make the path form A to B as long and meandering as possible. The premises on which its design is based are all askew, corrupted and self-contradicting.

They also give builders a chance to make lots of money. Or to lose lots of money.

Nowadays, we live in an age of mall software. People build startups, get financing, build crappy software and sometimes they hit it big (Twitter, Facebook) or, more likely, fade into obscurity leaving behind nothing at all, except money lost and sad programmers who spent nights coding stuff noone will ever see or use, and not much else.

Far from me saying startups are not a noble or worthy endeavour. They are! It's just that people who work on them should realize that they are not building software. That's why code doesn't look important to them, because they are actually selling eyeballs to advertisers, or collected personal data from their users to whoever buys that, or captive public for game developers, or whatever your business model says (if you have one!).

They are building malls, where the value is not in the building, which is pretty ghastly and useless by itself, but on the people in it, those who rent space in the mall, those who will use the mall, the software, the social network, whatever it is you are building.

Twitter is not software, Facebook is not software. If they were, identi.ca and diaspora would be bigger! What they are is people in one place, like a mall is not a real building, but a collection of people under a roof.

So, there is nothing wrong with building malls. Just remember that your ends and your means are one and a whole, that code is important, that without code Facebook and Twitter don't work, and that without people they are a badland, and know what you are doing.

Because the only hard thing in life is knowing what you want to do. The rest is the easy part. And because malls without toilets suck.

[1] If you really want to see the whole conversation, it's here: http://bettween.com/ralsina/iatrou (if anyone knows a better conversation tracker please post it in a comment).
[2] Yet here I am, an Engineering Manager at Canonical. Sorry guys!

Contents © 2000-2019 Roberto Alsina