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:
parent
466ecde71c
commit
5411f3fc2f
22
README
22
README
|
@ -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>
|
||||
|
||||
|
|
2
bin/gpo
2
bin/gpo
|
@ -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):
|
||||
|
|
1
makefile
1
makefile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue