Python 3: Initial support for Python 3 (CLI)

This changeset makes gPodder's codebase convertable
to Python 3 using the "2to3" utility. Right now, only
the CLI module (bin/gpo) has been tested.

See the README file for instructions and remarks.
This commit is contained in:
Thomas Perl 2012-01-10 13:47:20 +01:00
parent 466ecde71c
commit 5411f3fc2f
11 changed files with 100 additions and 41 deletions

22
README
View File

@ -85,6 +85,26 @@
To install gPodder system-wide, use "make install".
[ PYTHON 3 SUPPORT ]
The CLI version of gPodder (bin/gpo) is compatible with Python 3
after converting the codebase with the 2to3 utility:
2to3 -w bin/* src/gpodder/
You will also need a copy of "mygpoclient" converted using 2to3 and
a copy of "feedparser" converted using 2to3 (see the feedparser README
for details on how to get it set up on Python 3, including sgmllib).
Please note that the Gtk UI is not compatible with Python 3 (it will
be once we migrate the codebase to Gtk3/GObject Introspection), and
the QML UI - while theoretically compatible - has not been tested
with Python 3 yet due to the Python 3 support status in PySide.
As of January 2012, Python 3 support is still experimental. Please
report any bugs that you find to the gPodder bug tracker (see below).
[ PORTABLE MODE / ROAMING PROFILES ]
The run-time environment variable GPODDER_HOME is used to set
@ -104,5 +124,5 @@
- IRC channel #gpodder on irc.freenode.net
............................................................................
Last updated: 2012-01-09 by Thomas Perl <thp.io/about>
Last updated: 2012-01-10 by Thomas Perl <thp.io/about>

View File

@ -283,7 +283,7 @@ class gPodderCli(object):
episodes = (u'%3d. %s %s' % (i+1, status_str(e), e.title)
for i, e in enumerate(podcast.get_episodes()))
return episodes
return episodes
@FirstArgumentIsPodcastURL
def info(self, url):

View File

@ -136,6 +136,7 @@ clean:
$(PYTHON) setup.py clean
find src/ -name '*.pyc' -exec rm '{}' \;
find src/ -name '*.pyo' -exec rm '{}' \;
find src/ -type d -name '__pycache__' -exec rm -r '{}' \;
find data/ui/ -name '*.ui.h' -exec rm '{}' \;
rm -f MANIFEST PKG-INFO data/messages.pot~ $(DESKTOPFILE_H)
rm -f data/gpodder-??x??.png .coverage

View File

@ -88,8 +88,16 @@ osx = (platform.system() == 'Darwin')
textdomain = 'gpodder'
locale_dir = gettext.bindtextdomain(textdomain)
t = gettext.translation(textdomain, locale_dir, fallback=True)
gettext = t.ugettext
ngettext = t.ungettext
try:
# Python 2
gettext = t.ugettext
ngettext = t.ungettext
except AttributeError:
# Python 3
gettext = t.gettext
ngettext = t.ngettext
if win32:
try:
# Workaround for bug 650

View File

@ -37,6 +37,7 @@ import logging
logger = logging.getLogger(__name__)
from gpodder import schema
from gpodder import util
import threading
import re
@ -202,11 +203,8 @@ class Database(object):
with self.lock:
try:
cur = self.cursor()
def convert(x):
if isinstance(x, str):
x = x.decode('utf-8', 'ignore')
return x
values = [convert(getattr(o, name)) for name in columns]
values = [util.convert_bytes(getattr(o, name))
for name in columns]
if o.id is None:
qmarks = ', '.join('?'*len(columns))
@ -248,20 +246,20 @@ class Database(object):
Returns True if a foldername for a channel exists.
False otherwise.
"""
if not isinstance(foldername, unicode):
foldername = foldername.decode('utf-8', 'ignore')
foldername = util.convert_bytes(foldername)
return self.get("SELECT id FROM %s WHERE download_folder = ?" % self.TABLE_PODCAST, (foldername,)) is not None
return self.get("SELECT id FROM %s WHERE download_folder = ?" %
self.TABLE_PODCAST, (foldername,)) is not None
def episode_filename_exists(self, podcast_id, filename):
"""
Returns True if a filename for an episode exists.
False otherwise.
"""
if not isinstance(filename, unicode):
filename = filename.decode('utf-8', 'ignore')
filename = util.convert_bytes(filename)
return self.get("SELECT id FROM %s WHERE podcast_id = ? AND download_filename = ?" % self.TABLE_EPISODE, (podcast_id, filename,)) is not None
return self.get("SELECT id FROM %s WHERE podcast_id = ? AND download_filename = ?" %
self.TABLE_EPISODE, (podcast_id, filename,)) is not None
def get_last_published(self, podcast):
"""
@ -275,11 +273,10 @@ class Database(object):
a given channel. Used after feed updates for
episodes that have disappeared from the feed.
"""
if not isinstance(guid, unicode):
guid = guid.decode('utf-8', 'ignore')
guid = util.convert_bytes(guid)
with self.lock:
cur = self.cursor()
cur.execute('DELETE FROM %s WHERE podcast_id = ? AND guid = ?' % self.TABLE_EPISODE, \
(podcast_id, guid))
cur.execute('DELETE FROM %s WHERE podcast_id = ? AND guid = ?' %
self.TABLE_EPISODE, (podcast_id, guid))

View File

@ -46,7 +46,13 @@ import collections
import mimetypes
import email
import email.Header
try:
# Python 2
from email.Header import decode_header
except ImportError:
# Python 3
from email.header import decode_header
import cgi
@ -69,7 +75,7 @@ def get_header_param(headers, param, header_name):
value = msg.get_param(param, header=header_name)
if value is None:
return None
decoded_list = email.Header.decode_header(value)
decoded_list = decode_header(value)
value = []
for part, encoding in decoded_list:
if encoding:

View File

@ -88,6 +88,7 @@ class Store(object):
if isinstance(v, unicode):
return v
elif isinstance(v, str):
# XXX: Rewrite ^^^ as "isinstance(v, bytes)" in Python 3
return v.decode('utf-8')
else:
return str(v)

View File

@ -40,7 +40,14 @@ import glob
import shutil
import time
import datetime
import rfc822
try:
# Python 2
from rfc822 import mktime_tz
except ImportError:
# Python 3
from email.utils import mktime_tz
import hashlib
import feedparser
import collections
@ -212,7 +219,7 @@ class PodcastEpisode(PodcastModelObject):
episode.description = entry.get('subtitle', '')
if entry.get('updated_parsed', None):
episode.published = rfc822.mktime_tz(entry.updated_parsed+(0,))
episode.published = mktime_tz(entry.updated_parsed+(0,))
enclosures = entry.get('enclosures', [])
media_rss_content = entry.get('media_content', [])
@ -454,10 +461,7 @@ class PodcastEpisode(PodcastModelObject):
return _('No description available')
else:
# Decode the description to avoid gPodder bug 1277
if isinstance(desc, str):
desc = desc.decode('utf-8', 'ignore')
desc = desc.strip()
desc = util.convert_bytes(desc).strip()
if len(desc) > MAX_LINE_LENGTH:
return desc[:MAX_LINE_LENGTH] + '...'
@ -873,9 +877,7 @@ class PodcastChannel(PodcastModelObject):
@classmethod
def sort_key(cls, podcast):
key = podcast.title.lower()
if not isinstance(key, unicode):
key = key.decode('utf-8', 'ignore')
key = util.convert_bytes(podcast.title.lower())
return re.sub('^the ', '', key).translate(cls.UNICODE_TRANSLATE)
@classmethod

View File

@ -19,15 +19,15 @@
import gpodder
from gpodder import util
from PySide import QtCore
class Action(QtCore.QObject):
def __init__(self, caption, action, target=None):
QtCore.QObject.__init__(self)
if isinstance(caption, str):
caption = caption.decode('utf-8')
self._caption = caption
self._caption = util.convert_bytes(caption)
self.action = action
self.target = target

View File

@ -37,14 +37,7 @@ from gpodder import model
import threading
import os
def convert(s):
if s is None:
return None
if isinstance(s, unicode):
return s
return s.decode('utf-8', 'ignore')
convert = util.convert_bytes
class QEpisode(QObject):
def __init__(self, wrapper_manager, podcast, episode):

View File

@ -1182,6 +1182,33 @@ def open_website(url):
else:
threading.Thread(target=webbrowser.open, args=(url,)).start()
def convert_bytes(d):
"""
Convert byte strings to unicode strings
This function will decode byte strings into unicode
strings. Any other data types will be left alone.
>>> convert_bytes(None)
>>> convert_bytes(1)
1
>>> convert_bytes(True)
True
>>> convert_bytes(3.1415)
3.1415
>>> convert_bytes('Hello')
u'Hello'
>>> convert_bytes(u'Hey')
u'Hey'
"""
if d is None:
return d
if any(isinstance(d, t) for t in (int, bool, float)):
return d
elif not isinstance(d, unicode):
return d.decode('utf-8', 'ignore')
return d
def sanitize_encoding(filename):
r"""
Generate a sanitized version of a string (i.e.
@ -1290,12 +1317,16 @@ def find_mount_point(directory):
>>> restore()
"""
if isinstance(directory, unicode):
# XXX: This is only valid for Python 2 - misleading error in Python 3?
# We do not accept unicode strings, because they could fail when
# trying to be converted to some native encoding, so fail loudly
# and leave it up to the callee to encode into the proper encoding.
raise ValueError('Convert unicode objects to str first.')
if not isinstance(directory, str):
# In Python 2, we assume it's a byte str; in Python 3, we assume
# that it's a unicode str. The abspath/ismount/split functions of
# os.path work with unicode str in Python 3, but not in Python 2.
raise ValueError('Directory names should be of type str.')
directory = os.path.abspath(directory)