Skip to main content

Ralsina.Me — Roberto Alsina's website

Jack the Bodiless (Galactic Milieu Trilogy #1)

Review:

The pliocene ex­ile saga was one of my favourite sci­fi bok se­ries when I was a teenag­er. I had read it all in the wrong or­der be­cause find­ing the books in Ar­genti­na was not easy.

Thanks to goodreads I learned there were a bunch of oth­er books set in the same uni­verse and de­cid­ed read­ing them.

While not as en­joy­able, be­cause these as­sume lots of back­sto­ry, the writ­ing is en­joy­able, and it's fun. I sure would have loved to read these 20 years ago.

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

About Japan and God and Lilita

As ev­ery­one knows, there was a big quake in Japan, then a Tsunami, then a vol­cano erupt­ed, then a nu­cle­ar plant caught fire. All things con­sid­ered, a re­al­ly crap­py week.

Then again, if I were ja­pa­nese and I had to read id­iots telling me this was be­cause god is pu­bish­ing me be­cause of (what­ev­er the id­iot does­n't like about Japan), I would be sore­ly tempt­ed to find the mo­rons and ... ok, con­sid­er­ing the ja­pa­nese are show­ing they are very rea­son­able peo­ple, prob­a­bly just tell him some­thing po­lite­ly.

OTO­H, I am not ja­pa­nese. Which means I can ex­plain in great de­tail why those who say "maybe it's <what­ev­er> pun­ish­ing | telling japan <some­thing>" are a com­plete waste of oxy­gen.

I will fo­cus on one ex­am­ple, be­cause it's a very spe­cial re­li­gious mo­ron: a pres­i­den­tial can­di­date in Ar­genti­na, called Lili­ta Car­rió.

Here's what she said (s­pan­ish is the orig­i­nal, of course):

"Dios nos es­tá di­cien­do que debe­mos cuidar el plan­e­ta, que no sig­amos de­struyen­do la tier­ra, que vi­va­mos en la ver­dad, en la de­cen­ci­a, en la jus­ti­ci­a, que no use­mos la tec­nología, aunque sea de man­era pací­fi­ca. Hay que leer los sig­nos de los tiem­pos"

"God is telling us that we should take care of the plan­et, that we should stop de­stroy­ing the earth, that we should live in truth, in de­cen­cy, in jus­tice, and stop us­ing tech­nol­o­gy, even if it's peace­ful­ly. We should read the sign of the times".

Let's con­sid­er that lit­tle by lit­tle.

"God is telling us that we should take care of the plan­et"

I must con­fess I am amazed that an almight­ly be­ing is less ca­pa­ble of com­mu­ni­cat­ing ideas than my 3.9 year old kid. When he wants me to play bal­l, he brings the ball and tells me "Dad, let's play bal­l".

On the oth­er hand, god ap­par­ent­ly, to tell us to stop us­ing tech­nol­o­gy, caus­es a se­ries of cat­a­stroph­ic events in the oth­er end of the world, then brings us the news over the In­ter­net (a tech­no­log­i­cal mir­a­cle), so that Lili­ta can di­vine god's in­ten­tions and then re-broad­cast them to us over the ra­dio (of course, an er­ar­li­er tech­no­log­i­cal mir­a­cle).

Now, does that make sense to any­one? I mean, why does­n't god just, you know, say what he means in a rea­son­able man­ner? Be­cause for re­li­gious peo­ple, the fun is in the div­ina­tion. They are act­ing like ro­man priests di­vin­ing the fu­ture in the en­trails of an an­i­mal, ex­cept they are us­ing the life and suf­fer­ing of peo­ple.

Oh, look, suf­fer­ing in Japan, that means we should stop us­ing the Wi­i!

Not on­ly is that ap­proach com­plete­ly against ev­ery­thing chris­tian doc­trine teach­es, from with the virtue of char­i­ty (if god did it to tell us some­thing, by def­i­ni­tion they de­served it!) to the in­junc­tion against di­vin­ing god's mes­sages in por­tents (yes, it is for­bid­den, go ask a priest).

"[­God is telling us] that we should stop de­stroy­ing the earth"

Oh, gee, ok then! OTO­H, maybe a more sub­tle way than half-break­ing ev­ery­thing in a whole coun­try 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 be­liev­ing in god, who tru­ly does not ex­ist. When you catch up to that we'll ar­gue some more, ok?

"[­God is telling us to] stop us­ing tech­nol­o­gy, even if it's peace­ful­ly."

I would love if this pres­i­den­tial can­di­date did­n't use tech­nol­o­gy be­cause it would mean I would not have to see her sanc­ti­mo­nious stu­pid­i­ty ev­er again. OTO­H, if we would­n't have tech­nol­o­gy, we would prob­a­bly not know about the earth­quake yet. I sup­pose she may have been say­ing "nu­cle­ar tech­nol­o­gy" and this is out of con­tex­t.

OTO­H, num­ber of peo­ple killed by peace­ful nu­cle­ar tech­nol­o­gy since 1950: 1000? 10000?

num­ber of peo­ple killed by earth­quakes and tsuna­mi in the last 5 years: 100000? 200000?

Yes, those are num­bers I just made up, but I am bet­ting they are more right than wrong, so, ba­si­cal­ly, god has killed more peo­ple this week telling us not to use nu­cle­ar pow­er, than nu­cle­ar pow­er has killed in the last 50 years. Not ex­act­ly good com­mu­ni­ca­tion skill­s.

"We should read the sign of the times"

Ok, here it is:

The New York Times

Don't vote for this blither­ing id­iot. She's dan­ger­ous, and prob­a­bly men­tal­ly il­l.

New golfing challenge: PatoCabrera

In the spir­it of the De Vi­cen­zo web browser, I am start­ing a new pro­gram, called Pa­to Cabr­era. Here are the rules:

  • Twit­ter client (no iden­ti.­­ca in the first ver­­sion, but to be added lat­er)

  • Has these fea­­tures: http://­­paste­bin.lug­­men.org.ar/6464

  • Has to be im­­ple­­men­t­ed be­­fore April 4th

  • Smal­l­­er than 16384 bytes (of python code) but may be larg­er be­­cause of art­­work.

Let's see how it works :-)

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

I have al­ready post­ed a cou­ple of times (1, 2) about De Vi­cen­zo , an at­tempt to im­ple­ment the rest of the browser, start­ing with PyQt's We­bKit... lim­it­ing my­self to 128 lines of code.

Of course I could do more, but I have my stan­dard­s!

  • No us­ing ;

  • No if what­ev­er: f()

Oth­er than that, I did a lot of dirty trick­s, but right now, it's a fair­ly com­plete browser, and it has 127 lines of code (ac­cord­ing to sloc­coun­t) so that's enough play­ing and it's time to go back to re­al work.

But first, let's con­sid­er how some fea­tures were im­ple­ment­ed (I'll wrap the lines so they page stays rea­son­ably nar­row), and al­so look at the "nor­mal" ver­sions of the same (the "nor­mal" code is not test­ed, please tell me if it's bro­ken ;-).

This is not some­thing you should learn how to do. In fac­t, this is al­most a trea­tise on how not to do things. This is some of the least python­ic, less clear code you will see this week.

It is short, and it is ex­pres­sive. But it is ug­ly.

I'll dis­cuss this ver­sion.

Proxy Support

A brows­er is not much of a brows­er if you can't use it with­out a prox­y, but luck­i­ly Qt's net­work stack has good proxy sup­port. The trick was con­fig­ur­ing 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 nor­mal 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 abus­es against python here are the use of the ternary op­er­a­tor as a one-­line if (and nest­ing it), and line length.

Persistent Cookies

You re­al­ly need this, since you want to stay logged in­to your sites be­tween ses­sion­s. For this, first I need­ed to write some per­sis­tence mech­a­nis­m, and then save/re­store the cook­ies 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 con­fess I am guilty of us­ing list com­pre­hen­sions when a for loop would have been the cor­rect thing.

I use the same trick when restor­ing the open tab­s, with the added mis­fea­ture of us­ing a list com­pre­hen­sion and throw­ing away the re­sult:

# 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 fea­ture of re­cent PyQt ver­sion­s: if you pass prop­er­ty names as key­word ar­gu­ments when you cre­ate an ob­jec­t, they are as­signed the val­ue. If you pass a sig­nal as a key­word ar­gu­men­t, they are con­nect­ed to the giv­en val­ue.

This is a re­al­ly great fea­ture that helps you cre­ate clear, lo­cal code, and it's a great thing to have. But if you are writ­ing evil code... well, you can go to hell on a hand­bas­ket us­ing it.

This is all over the place in De Vi­cen­zo, and here's one ex­am­ple (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 lamb­das that con­tain the ternary op­er­a­tor:

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 un­tan­gle this for ed­u­ca­tion­al pur­pos­es, but let's just say that line con­tains what should be re­placed by 3 meth­od­s, and should be spread over 6 lines or more.

Download Manager

Ok, call­ing it a man­ag­er is over­reach­ing, since you can't stop them once they start, but hey, it lets you down­load things and keep on brows­ing, and re­ports the pro­gress!

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

Then, I need­ed to del­e­gate the un­sup­port­ed con­tent 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 re­al code golf­ing here, ex­cept for long lines, but once you break them rea­son­ably, this is pret­ty much the ob­vi­ous way to do it:

  • Ask for a file­­name

  • Cre­ate a pro­­gress­bar, put it in the sta­­tus­bar, and con­nect it to the down­load­­'s progress sig­­nal­s.

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 de­fined a method as a lamb­da 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()))

No­tice that it even han­dles redi­rec­tions sane­ly! Be­yond that, it just hides the progress bar, saves the data, end of sto­ry. The long­est line is not even my fault!

There is a big in­ef­fi­cien­cy in that the whole file is kept in mem­o­ry un­til the end. If you down­load a DVD im­age, 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, be­cause do­ing this man­u­al­ly would have been a pain. How­ev­er, it turns out that print­ing is just ... there? Qt, spe­cial­ly when used via PyQt is such an awe­some­ly rich en­vi­ron­men­t.

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 oth­er trick­s. All that's left is cre­at­ing wid­get­s, con­nect­ing things to one an­oth­er, and en­joy­ing the awe­some ex­pe­ri­ence of pro­gram­ming PyQt, where you can write a whole web brows­er (ex­cept the en­gine) in 127 lines of code.


Contents © 2000-2021 Roberto Alsina