From c087a6b49b154634f1aaa88e6741ad96e261221f Mon Sep 17 00:00:00 2001 From: Frank Fenton Date: Mon, 17 Sep 2012 22:46:29 +1000 Subject: [PATCH 001/127] Add Trakt notification --- .../core/notifications/trakt/__init__.py | 43 +++++++++++++++++ couchpotato/core/notifications/trakt/main.py | 48 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 couchpotato/core/notifications/trakt/__init__.py create mode 100644 couchpotato/core/notifications/trakt/main.py diff --git a/couchpotato/core/notifications/trakt/__init__.py b/couchpotato/core/notifications/trakt/__init__.py new file mode 100644 index 00000000..5a3ec725 --- /dev/null +++ b/couchpotato/core/notifications/trakt/__init__.py @@ -0,0 +1,43 @@ +from .main import Trakt + +def start(): + return Trakt() + +config = [{ + 'name': 'trakt', + 'groups': [ + { + 'tab': 'notifications', + 'name': 'trakt_notification', + 'label': 'Trakt', + 'description': 'add movies to your collection once downloaded', + 'options': [ + { + 'name': 'notification_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'remove_watchlist_enabled', + 'label': 'Remove from watchlist', + 'default': False, + 'type': 'bool', + }, + { + 'name': 'automation_api_key', + 'label': 'Apikey', + }, + { + 'name': 'automation_username', + 'label': 'Username', + }, + { + 'name': 'automation_password', + 'label': 'Password', + 'type': 'password', + 'description': 'Required even if your account is unprotected.', + }, + ], + } + ], +}] \ No newline at end of file diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt/main.py new file mode 100644 index 00000000..94fecb11 --- /dev/null +++ b/couchpotato/core/notifications/trakt/main.py @@ -0,0 +1,48 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +import json +import urllib2 + +log = CPLog(__name__) + +class Trakt(Notification): + + urls = { + 'base': 'http://api.trakt.tv/', + 'library': 'movie/library/%s', + 'unwatchlist': 'movie/unwatchlist/%s', + } + + listen_to = ['movie.downloaded'] + + def notify(self, message = '', data = {}, listener = None): + if self.isDisabled(): return + if not data: return + post_data = { + 'username': self.conf('automation_username'), + 'password' : self.conf('automation_password'), + 'movies': [ { + 'imdb_id': data['library']['identifier'], + 'title': data['library']['titles'][0]['title'], + 'year': data['library']['year'] + } ] + } + result = False + result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data) + if self.conf('remove_watchlist_enabled'): + result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data) + + return result + + def call(self, method_url, post_data): + log.info('opening url: ' + self.urls['base'] + method_url + ', post: ' + str(post_data)) + try: + response = urllib2.urlopen(self.urls['base'] + method_url, json.dumps(post_data)) + if response: + if json.load(response).get('status') == "success": + log.info('Successfully called Trakt') + return True + except: + pass + log.error('Failed to call trakt, check your login.') + return False \ No newline at end of file From 9500ac73fc70bf8dbb2ec99b44fdcbcc1f5708a6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 2 Jan 2013 13:52:44 +0100 Subject: [PATCH 002/127] Link to downloaders --- couchpotato/core/downloaders/nzbget/__init__.py | 2 +- couchpotato/core/downloaders/nzbvortex/__init__.py | 2 +- couchpotato/core/downloaders/pneumatic/__init__.py | 2 +- couchpotato/core/downloaders/sabnzbd/__init__.py | 2 +- couchpotato/core/downloaders/synology/__init__.py | 2 +- couchpotato/core/downloaders/transmission/__init__.py | 2 +- couchpotato/core/downloaders/utorrent/__init__.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 10994299..4f5afb36 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'nzbget', 'label': 'NZBGet', - 'description': 'Send NZBs to your NZBGet installation.', + 'description': 'Use NZBGet to download NZBs.', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/downloaders/nzbvortex/__init__.py b/couchpotato/core/downloaders/nzbvortex/__init__.py index d79983f4..ce9c8587 100644 --- a/couchpotato/core/downloaders/nzbvortex/__init__.py +++ b/couchpotato/core/downloaders/nzbvortex/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'nzbvortex', 'label': 'NZBVortex', - 'description': 'Send NZBs to your NZBVortex app.', + 'description': 'Use NZBVortex to download NZBs.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/pneumatic/__init__.py b/couchpotato/core/downloaders/pneumatic/__init__.py index 004821c5..f119cfc5 100644 --- a/couchpotato/core/downloaders/pneumatic/__init__.py +++ b/couchpotato/core/downloaders/pneumatic/__init__.py @@ -11,7 +11,7 @@ config = [{ 'tab': 'downloaders', 'name': 'pneumatic', 'label': 'Pneumatic', - 'description': 'Download the .strm file to a specific folder.', + 'description': 'Use Pneumatic to download .strm files.', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py index 927a9ff2..e4162509 100644 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'sabnzbd', 'label': 'Sabnzbd', - 'description': 'Send NZBs to your Sabnzbd installation.', + 'description': 'Use SABnzbd to download NZBs.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/synology/__init__.py b/couchpotato/core/downloaders/synology/__init__.py index 2b4757a5..2b7e861d 100644 --- a/couchpotato/core/downloaders/synology/__init__.py +++ b/couchpotato/core/downloaders/synology/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'synology', 'label': 'Synology', - 'description': 'Send torrents to Synology\'s Download Station.', + 'description': 'Use Synology Download Station to download.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 189fcd91..0fe11845 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'transmission', 'label': 'Transmission', - 'description': 'Send torrents to Transmission.', + 'description': 'Use Transmission to download torrents.', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/downloaders/utorrent/__init__.py b/couchpotato/core/downloaders/utorrent/__init__.py index 88ceaf71..09a82a1e 100644 --- a/couchpotato/core/downloaders/utorrent/__init__.py +++ b/couchpotato/core/downloaders/utorrent/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'downloaders', 'name': 'utorrent', 'label': 'uTorrent', - 'description': 'Send torrents to uTorrent.', + 'description': 'Use uTorrent to download torrents.', 'wizard': True, 'options': [ { From 393c14de5466497eb51d10bfe1f65febad658535 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 7 Jan 2013 23:10:06 +0100 Subject: [PATCH 003/127] Urlencode spotweb id. fix #1213 --- couchpotato/core/providers/nzb/newznab/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index f1f0d48e..b1e93bcd 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -73,8 +73,8 @@ class Newznab(NZBProvider, RSS): 'name': self.getTextElement(nzb, 'title'), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024, - 'url': (self.getUrl(host['host'], self.urls['download']) % nzb_id) + self.getApiExt(host), - 'detail_url': '%sdetails/%s' % (cleanHost(host['host']), nzb_id), + 'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), + 'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)), 'content': self.getTextElement(nzb, 'description'), }) From ab53f44157b99f97a4a21e22ffadc8421b2e2a5f Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 23 Jan 2013 22:23:52 +0100 Subject: [PATCH 004/127] Remove non-int backup folders. closes #1298 --- couchpotato/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 7062c75b..97d15a55 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -98,7 +98,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En total_backups = len(backups) for backup in backups: if total_backups > 3: - if int(os.path.basename(backup)) < time.time() - 259200: + if tryInt(os.path.basename(backup)) < time.time() - 259200: for src_file in src_files: b_file = os.path.join(backup, os.path.basename(src_file)) if os.path.isfile(b_file): From a49a00a25f5c0a13d543c767cb77efbd61ddfe5d Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 14 Feb 2013 23:01:34 +0100 Subject: [PATCH 005/127] Host to 0.0.0.0 --- couchpotato/runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 37152582..66a524cc 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -209,11 +209,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Basic config app.secret_key = api_key + host = Env.setting('host', default = '0.0.0.0') # app.debug = development config = { 'use_reloader': reloader, 'port': tryInt(Env.setting('port', default = 5000)), - 'host': Env.setting('host', default = ''), + 'host': host if host and len(host) > 0 else '0.0.0.0', 'ssl_cert': Env.setting('ssl_cert', default = None), 'ssl_key': Env.setting('ssl_key', default = None), } From fab8e66fe12e0fe046e298a1685ddb2574a380e8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 17 Mar 2013 16:39:09 +0100 Subject: [PATCH 006/127] One up Conflicts: version.py --- version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.py b/version.py index 761ffb25..3ad9115a 100644 --- a/version.py +++ b/version.py @@ -1,2 +1,2 @@ -VERSION = '2.0.6' +VERSION = '2.0.7' BRANCH = 'master' From 0115bf254e84062db9eea257cec19710bce7019d Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 19 Mar 2013 22:55:10 +0100 Subject: [PATCH 007/127] Force default profile on movies without profile. fix #1523 --- couchpotato/core/plugins/profile/main.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 4caa54f7..3dd89d9f 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -5,7 +5,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.request import jsonified, getParams, getParam from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Profile, ProfileType +from couchpotato.core.settings.model import Profile, ProfileType, Movie log = CPLog(__name__) @@ -30,6 +30,21 @@ class ProfilePlugin(Plugin): }) addEvent('app.initialize', self.fill, priority = 90) + addEvent('app.load', self.forceDefaults) + + def forceDefaults(self): + + # Get all active movies without profile + active_status = fireEvent('status.get', 'active', single = True) + + db = get_session() + movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all() + + if len(movies) > 0: + default_profile = self.default() + for movie in movies: + movie.profile_id = default_profile.get('id') + db.commit() def allView(self): @@ -129,6 +144,9 @@ class ProfilePlugin(Plugin): db.delete(p) db.commit() + # Force defaults on all empty profile movies + self.forceDefaults() + success = True except Exception, e: message = log.error('Failed deleting Profile: %s', e) From 7f24563bba4343034b41aba76004c3fa22751c2b Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Mon, 18 Mar 2013 11:46:17 +0100 Subject: [PATCH 008/127] Add Advanced Option to deactivate "Too early to search for ..." Advanced Option: Check Released --- couchpotato/core/_base/_core/__init__.py | 7 +++++++ couchpotato/core/plugins/searcher/main.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index c8c3fda6..8b24ca21 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -94,6 +94,13 @@ config = [{ 'label': 'File CHMOD', 'description': 'Same as Folder CHMOD but for files', }, + { + 'name': 'could_be_released', + 'default': 1, + 'type': 'bool', + 'label': 'Check Released', + 'description': 'Don\'t search if not released yet.', + }, ], }, ], diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 1f320eab..68a4eabe 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -157,7 +157,7 @@ class Searcher(Plugin): ret = False for quality_type in movie['profile']['types']: - if not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates): + if Env.setting('could_be_released') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates): log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title)) continue From 250236bd257dc143f82c7dd868f3aea62359de5a Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 19 Mar 2013 23:12:47 +0100 Subject: [PATCH 009/127] Option to force search --- couchpotato/core/_base/_core/__init__.py | 7 ------- couchpotato/core/plugins/searcher/__init__.py | 8 ++++++++ couchpotato/core/plugins/searcher/main.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index 8b24ca21..c8c3fda6 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -94,13 +94,6 @@ config = [{ 'label': 'File CHMOD', 'description': 'Same as Folder CHMOD but for files', }, - { - 'name': 'could_be_released', - 'default': 1, - 'type': 'bool', - 'label': 'Check Released', - 'description': 'Don\'t search if not released yet.', - }, ], }, ], diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index a6dd6913..90cece46 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -40,6 +40,14 @@ config = [{ 'type': 'dropdown', 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], }, + { + 'name': 'always_search', + 'default': False, + 'advanced': True, + 'type': 'bool', + 'label': 'Always search', + 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.', + }, ], }, { 'tab': 'searcher', diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 68a4eabe..b4c4937a 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -157,7 +157,7 @@ class Searcher(Plugin): ret = False for quality_type in movie['profile']['types']: - if Env.setting('could_be_released') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates): + if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates): log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title)) continue From 2cb6ddfe9a89757915c5722221e090d2b43cc12a Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sat, 16 Mar 2013 14:29:33 +0100 Subject: [PATCH 010/127] Add automation genre checking With this commit you can set requirements to the genres of movie automation downloading. Required sets e.g. Action&Crime and/or ignored sets e.g. Romance&Comedy. --- .../core/plugins/automation/__init__.py | 14 ++++++++++++ couchpotato/core/providers/automation/base.py | 22 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py index b7b1ab28..baff29c4 100644 --- a/couchpotato/core/plugins/automation/__init__.py +++ b/couchpotato/core/plugins/automation/__init__.py @@ -36,6 +36,20 @@ config = [{ 'unit': 'hours', 'description': 'hours', }, + { + 'name': 'required_genres', + 'label': 'Required Genres', + 'default': '', + 'placeholder': 'Example: Action, Crime & Drama', + 'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"' + }, + { + 'name': 'ignored_genres', + 'label': 'Ignored Genres', + 'default': '', + 'placeholder': 'Example: Horror, Comedy & Drama & Romance', + 'description': 'Ignore movies that contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"' + }, ], }, ], diff --git a/couchpotato/core/providers/automation/base.py b/couchpotato/core/providers/automation/base.py index 8d08f913..6aef58a1 100644 --- a/couchpotato/core/providers/automation/base.py +++ b/couchpotato/core/providers/automation/base.py @@ -2,6 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import Provider from couchpotato.environment import Env +from couchpotato.core.helpers.variable import splitString import time log = CPLog(__name__) @@ -59,7 +60,26 @@ class Automation(Provider): type_value = movie.get(minimal_type, 0) type_min = self.getMinimal(minimal_type) if type_value < type_min: - log.info('%s too low for %s, need %s has %s', (minimal_type, movie['imdb'], type_min, type_value)) + log.info('%s too low for %s, need %s has %s', (minimal_type, movie['original_title'], type_min, type_value)) + return False + + movie_genres = [genre.lower() for genre in movie['genres']] + required_genres = splitString(self.getMinimal('required_genres').lower()) + ignored_genres = splitString(self.getMinimal('ignored_genres').lower()) + + req_match = 0 + for req_set in required_genres: + req = splitString(req_set, '&') + req_match += len(list(set(movie_genres) & set(req))) == len(req) + + if self.getMinimal('required_genres') and req_match == 0: + log.info2("Required genre(s) missing for %s" % movie['original_title']) + return False + + for ign_set in ignored_genres: + ign = splitString(ign_set, '&') + if len(list(set(movie_genres) & set(ign))) == len(ign): + log.info2("%s has blacklisted genre(s): %s" % (movie['original_title'], ign)) return False return True From 572dfd529ed1b9a20f12c40d36aeddebdf76fa95 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 19 Mar 2013 23:26:55 +0100 Subject: [PATCH 011/127] Shorten automation description --- couchpotato/core/plugins/automation/__init__.py | 2 +- couchpotato/static/style/settings.css | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py index baff29c4..440232b2 100644 --- a/couchpotato/core/plugins/automation/__init__.py +++ b/couchpotato/core/plugins/automation/__init__.py @@ -48,7 +48,7 @@ config = [{ 'label': 'Ignored Genres', 'default': '', 'placeholder': 'Example: Horror, Comedy & Drama & Romance', - 'description': 'Ignore movies that contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"' + 'description': 'Ignore movies that contain at least one set of genres. Sets work the same as above.' }, ], }, diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index 3af7dba7..6d3a8eb6 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -133,6 +133,7 @@ display: inline-block; vertical-align: middle; padding-left: 2%; + line-height: 14px; } .page .check + .formHint { @@ -142,6 +143,7 @@ padding-left: 1% !important; height: 24px; vertical-align: middle; + line-height: 24px; } .page .ctrlHolder label { From 4406f133b92213d2f3df2e247b361f7daa0149db Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 19 Mar 2013 23:28:51 +0100 Subject: [PATCH 012/127] CAPITALIZE MOTHAFAAACKAAH! --- couchpotato/core/plugins/manage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/manage/__init__.py b/couchpotato/core/plugins/manage/__init__.py index 30f6ea68..46eee210 100644 --- a/couchpotato/core/plugins/manage/__init__.py +++ b/couchpotato/core/plugins/manage/__init__.py @@ -8,7 +8,7 @@ config = [{ 'groups': [ { 'tab': 'manage', - 'label': 'movie library manager', + 'label': 'Movie Library Manager', 'description': 'Add your existing movie folders.', 'options': [ { From 4aff3f04954602b5eacd486ea394bd6a578647c3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 20 Mar 2013 08:50:43 +0100 Subject: [PATCH 013/127] Add score per provider. closes #1512 --- couchpotato/core/plugins/score/scores.py | 10 +++++----- .../core/providers/nzb/binsearch/__init__.py | 8 ++++++++ .../core/providers/nzb/ftdworld/__init__.py | 8 ++++++++ .../core/providers/nzb/newznab/__init__.py | 9 ++++++++- couchpotato/core/providers/nzb/newznab/main.py | 7 +++++-- .../core/providers/nzb/nzbclub/__init__.py | 8 ++++++++ .../core/providers/nzb/nzbindex/__init__.py | 8 ++++++++ .../core/providers/nzb/nzbsrus/__init__.py | 8 ++++++++ couchpotato/core/providers/nzb/nzbx/__init__.py | 8 ++++++++ .../core/providers/nzb/omgwtfnzbs/__init__.py | 8 ++++++++ .../core/providers/torrent/iptorrents/__init__.py | 8 ++++++++ .../providers/torrent/kickasstorrents/__init__.py | 8 ++++++++ .../providers/torrent/passthepopcorn/__init__.py | 8 ++++++++ .../core/providers/torrent/publichd/__init__.py | 8 ++++++++ .../providers/torrent/sceneaccess/__init__.py | 8 ++++++++ .../core/providers/torrent/scenehd/__init__.py | 8 ++++++++ .../providers/torrent/thepiratebay/__init__.py | 8 ++++++++ .../core/providers/torrent/torrentday/__init__.py | 8 ++++++++ .../providers/torrent/torrentleech/__init__.py | 8 ++++++++ couchpotato/static/style/settings.css | 15 ++++++++++++++- 20 files changed, 160 insertions(+), 9 deletions(-) diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py index 3d5a9e69..c9345fc0 100644 --- a/couchpotato/core/plugins/score/scores.py +++ b/couchpotato/core/plugins/score/scores.py @@ -116,13 +116,13 @@ def sizeScore(size): def providerScore(provider): - if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']: - return 20 - if provider in ['Newznab']: - return 10 + try: + score = tryInt(Env.setting('extra_score', section = provider.lower(), default = 0)) + except: + score = 0 - return 0 + return score def duplicateScore(nzb_name, movie_name): diff --git a/couchpotato/core/providers/nzb/binsearch/__init__.py b/couchpotato/core/providers/nzb/binsearch/__init__.py index f4288b11..d3288604 100644 --- a/couchpotato/core/providers/nzb/binsearch/__init__.py +++ b/couchpotato/core/providers/nzb/binsearch/__init__.py @@ -18,6 +18,14 @@ config = [{ 'name': 'enabled', 'type': 'enabler', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/nzb/ftdworld/__init__.py b/couchpotato/core/providers/nzb/ftdworld/__init__.py index ca60ac4d..37aedb2a 100644 --- a/couchpotato/core/providers/nzb/ftdworld/__init__.py +++ b/couchpotato/core/providers/nzb/ftdworld/__init__.py @@ -27,6 +27,14 @@ config = [{ 'default': '', 'type': 'password', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index 9047d200..2349590f 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -30,13 +30,20 @@ config = [{ 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info', 'description': 'The hostname of your newznab provider', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'default': '0,0,0,0', + 'description': 'Starting score for each release found via this provider.', + }, { 'name': 'api_key', 'default': ',,,', 'label': 'Api Key', 'description': 'Can be found on your profile page', 'type': 'combined', - 'combine': ['use', 'host', 'api_key'], + 'combine': ['use', 'host', 'api_key', 'extra_score'], }, ], }, diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index 414e7112..11107c1f 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -1,6 +1,6 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.rss import RSS -from couchpotato.core.helpers.variable import cleanHost, splitString +from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import ResultList from couchpotato.core.providers.nzb.base import NZBProvider @@ -76,6 +76,7 @@ class Newznab(NZBProvider, RSS): 'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), 'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)), 'content': self.getTextElement(nzb, 'description'), + 'score': host['extra_score'], }) def getHosts(self): @@ -83,13 +84,15 @@ class Newznab(NZBProvider, RSS): uses = splitString(str(self.conf('use'))) hosts = splitString(self.conf('host')) api_keys = splitString(self.conf('api_key')) + extra_score = splitString(self.conf('extra_score')) list = [] for nr in range(len(hosts)): list.append({ 'use': uses[nr], 'host': hosts[nr], - 'api_key': api_keys[nr] + 'api_key': api_keys[nr], + 'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0 }) return list diff --git a/couchpotato/core/providers/nzb/nzbclub/__init__.py b/couchpotato/core/providers/nzb/nzbclub/__init__.py index 7859fe9c..9955462c 100644 --- a/couchpotato/core/providers/nzb/nzbclub/__init__.py +++ b/couchpotato/core/providers/nzb/nzbclub/__init__.py @@ -18,6 +18,14 @@ config = [{ 'name': 'enabled', 'type': 'enabler', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/nzb/nzbindex/__init__.py b/couchpotato/core/providers/nzb/nzbindex/__init__.py index 29eb0d38..aa8de4dd 100644 --- a/couchpotato/core/providers/nzb/nzbindex/__init__.py +++ b/couchpotato/core/providers/nzb/nzbindex/__init__.py @@ -19,6 +19,14 @@ config = [{ 'type': 'enabler', 'default': True, }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/nzb/nzbsrus/__init__.py b/couchpotato/core/providers/nzb/nzbsrus/__init__.py index 3a042784..863e6a37 100644 --- a/couchpotato/core/providers/nzb/nzbsrus/__init__.py +++ b/couchpotato/core/providers/nzb/nzbsrus/__init__.py @@ -35,6 +35,14 @@ config = [{ 'label': 'English only', 'description': 'Only search for English spoken movies on Nzbsrus', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/nzb/nzbx/__init__.py b/couchpotato/core/providers/nzb/nzbx/__init__.py index 9ce9226b..9cf80638 100644 --- a/couchpotato/core/providers/nzb/nzbx/__init__.py +++ b/couchpotato/core/providers/nzb/nzbx/__init__.py @@ -19,6 +19,14 @@ config = [{ 'type': 'enabler', 'default': True, }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py b/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py index 287ced49..fe80518c 100644 --- a/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py +++ b/couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py @@ -27,6 +27,14 @@ config = [{ 'label': 'Api Key', 'default': '', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'default': 20, + 'type': 'int', + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/torrent/iptorrents/__init__.py b/couchpotato/core/providers/torrent/iptorrents/__init__.py index bca8ce69..24f9772b 100644 --- a/couchpotato/core/providers/torrent/iptorrents/__init__.py +++ b/couchpotato/core/providers/torrent/iptorrents/__init__.py @@ -34,6 +34,14 @@ config = [{ 'type': 'bool', 'description': 'Only search for [FreeLeech] torrents.', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py index 8ddb1f4a..999dbb1b 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py @@ -19,6 +19,14 @@ config = [{ 'type': 'enabler', 'default': True, }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py index 06be7a89..a9f687c3 100644 --- a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py +++ b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py @@ -37,6 +37,14 @@ config = [{ { 'name': 'passkey', 'default': '', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', } ], } diff --git a/couchpotato/core/providers/torrent/publichd/__init__.py b/couchpotato/core/providers/torrent/publichd/__init__.py index 2c356e20..3c27cf48 100644 --- a/couchpotato/core/providers/torrent/publichd/__init__.py +++ b/couchpotato/core/providers/torrent/publichd/__init__.py @@ -19,6 +19,14 @@ config = [{ 'type': 'enabler', 'default': True, }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/providers/torrent/sceneaccess/__init__.py index e12bf8bd..baad57f6 100644 --- a/couchpotato/core/providers/torrent/sceneaccess/__init__.py +++ b/couchpotato/core/providers/torrent/sceneaccess/__init__.py @@ -28,6 +28,14 @@ config = [{ 'default': '', 'type': 'password', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/torrent/scenehd/__init__.py b/couchpotato/core/providers/torrent/scenehd/__init__.py index 10c5e385..3cd2132e 100644 --- a/couchpotato/core/providers/torrent/scenehd/__init__.py +++ b/couchpotato/core/providers/torrent/scenehd/__init__.py @@ -28,6 +28,14 @@ config = [{ 'default': '', 'type': 'password', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/providers/torrent/thepiratebay/__init__.py index 38169084..f2394dd6 100644 --- a/couchpotato/core/providers/torrent/thepiratebay/__init__.py +++ b/couchpotato/core/providers/torrent/thepiratebay/__init__.py @@ -24,6 +24,14 @@ config = [{ 'advanced': True, 'label': 'Proxy server', 'description': 'Domain for requests, keep empty to let CouchPotato pick.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', } ], } diff --git a/couchpotato/core/providers/torrent/torrentday/__init__.py b/couchpotato/core/providers/torrent/torrentday/__init__.py index 1a4d3c7f..de715b53 100644 --- a/couchpotato/core/providers/torrent/torrentday/__init__.py +++ b/couchpotato/core/providers/torrent/torrentday/__init__.py @@ -28,6 +28,14 @@ config = [{ 'default': '', 'type': 'password', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/core/providers/torrent/torrentleech/__init__.py b/couchpotato/core/providers/torrent/torrentleech/__init__.py index d96ac064..fa048d50 100644 --- a/couchpotato/core/providers/torrent/torrentleech/__init__.py +++ b/couchpotato/core/providers/torrent/torrentleech/__init__.py @@ -28,6 +28,14 @@ config = [{ 'default': '', 'type': 'password', }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } ], }, ], diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index 6d3a8eb6..a5655f88 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -515,8 +515,21 @@ display: none; } .page .combined_table .head abbr.host { - margin-right: 197px; + margin-right: 190px; } + .page .combined_table .head abbr.api_key { + margin-right: 171px; + } + .page .combined_table .head .extra_score, + .page .combined_table .extra_score { + width: 70px; + text-align: center; + display: none; + } + .page.show_advanced .combined_table .head .extra_score, + .page.show_advanced .combined_table .extra_score { + display: inline-block; + } .page .combined_table .ctrlHolder { padding-top: 2px; From b843d5f13bea08543a2efc82840e2a6ad73c6063 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 20 Mar 2013 19:35:11 +0100 Subject: [PATCH 014/127] General notification icons --- couchpotato/core/notifications/base.py | 3 +++ couchpotato/core/notifications/pushalot/main.py | 4 ++-- couchpotato/core/notifications/xbmc/main.py | 7 +++---- .../static/images/notify.couch.large.png | Bin 0 -> 96881 bytes .../static/images/notify.couch.medium.png | Bin 0 -> 33804 bytes .../{xbmc-notify.png => notify.couch.small.png} | Bin 6 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 couchpotato/static/images/notify.couch.large.png create mode 100644 couchpotato/static/images/notify.couch.medium.png rename couchpotato/static/images/{xbmc-notify.png => notify.couch.small.png} (100%) diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 7e98fa51..2604ef47 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -37,6 +37,9 @@ class Notification(Plugin): return notify + def getNotificationImage(self, size = 'small'): + return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size + def notify(self, message = '', data = {}, listener = None): pass diff --git a/couchpotato/core/notifications/pushalot/main.py b/couchpotato/core/notifications/pushalot/main.py index 3b113311..1c82a25a 100644 --- a/couchpotato/core/notifications/pushalot/main.py +++ b/couchpotato/core/notifications/pushalot/main.py @@ -18,10 +18,10 @@ class Pushalot(Notification): 'AuthorizationToken': self.conf('auth_token'), 'Title': self.default_title, 'Body': toUnicode(message), - 'LinkTitle': toUnicode("CouchPotato"), - 'link': toUnicode("https://couchpota.to/"), 'IsImportant': self.conf('important'), 'IsSilent': self.conf('silent'), + 'Image': toUnicode(self.getNotificationImage('medium')), + 'Source': toUnicode(self.default_title) } headers = { diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index a1987bfa..0187a833 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -13,7 +13,6 @@ class XBMC(Notification): listen_to = ['renamer.after'] use_json_notifications = {} - couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png' def notify(self, message = '', data = {}, listener = None): if self.isDisabled(): return @@ -28,7 +27,7 @@ class XBMC(Notification): if self.use_json_notifications.get(host): response = self.request(host, [ - ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}), + ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), ('VideoLibrary.Scan', {}), ]) else: @@ -90,7 +89,7 @@ class XBMC(Notification): self.use_json_notifications[host] = True # send the text message - resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})]) + resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})]) for result in resp: if (result.get('result') and result['result'] == 'OK'): log.debug('Message delivered successfully!') @@ -113,7 +112,7 @@ class XBMC(Notification): server = 'http://%s/xbmcCmds/' % host # Notification(title, message [, timeout , image]) - cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url)) + cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium'))) server += cmd # I have no idea what to set to, just tried text/plain and seems to be working :) diff --git a/couchpotato/static/images/notify.couch.large.png b/couchpotato/static/images/notify.couch.large.png new file mode 100644 index 0000000000000000000000000000000000000000..e64347bd4ff9f63cac2c781c87e8139f8b3cb7ba GIT binary patch literal 96881 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�c2d&$~6|iUSrGe(xdC+W-a%_iI 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 YUnV4fCS)Dg0sbzW_4>K-r)zirKaGt{Z2$lO literal 0 HcmV?d00001 diff --git a/couchpotato/static/images/notify.couch.medium.png b/couchpotato/static/images/notify.couch.medium.png new file mode 100644 index 0000000000000000000000000000000000000000..c02974dea53917bdcc188b6132c630425c54d22d GIT binary patch literal 33804 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

)lpLUyZ|HWP26c-ymXzO6)24`NJ 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 z

l^**UFqGF?v$%}6wvHN`yKwxAf+VZp1V6?B0KD*W~|w(0XMkcvEsv? z_}|j=nov4#iYSGA4e==RhK1_ij-B9)Ilh-A^IWVpCq4{Al8L>v`ggXASjuCp?K{k} zqOECn^2GGJ&|9X6hBh71vcAEVo>w(aG1Kufxk$KlhU`(WSiE&{t7j-pR0Fl*S>U~z)fT-RltanG zBu`bmL%;vLySL3PJ>Cr{K9IlpFIS~a^90^|C5ObK)wKNllv~>Ysj|G;XzM|JG;xYL zKcB9KH(eZ&cK&PW@VgPKGYgM=u)qeW_$*PsXO!WCavrb88 zM|3Fi*1{o(CpPQ=W*c!lB{|<1dFc9hfWOL6=7xiqXUrxEnxZj=JBgQ#)v6H=KAti- z=82;FsK=$yK>l_V@uKRT=$VP$Tpvh4f8|OxBb(~|z*WE;;3&dzTh?L9sJ}Zu(o8#w z(R7i7>jG_F;g#f^Nar&{oGQyZES2kb-^>7bhPHe;NXQP$FsAP_QC??`Mr?b46J5ND z^J=(v+T{Qsm zbF~@2N#VRq-1Q}`4z!T$%*@GhP-lpIl3L=LE#tWw{wRzaJ7uf#hEaYRYV2}$XP%!7 z<%36)D@)%UONhkiE>%%@hIqIKW9`0mb-_Z>b#DZmB~B@DmjyQ^3KEx%f0do%FjF_) zVApS1AlPJ_#<98T7K#YUOJ&ZF=>$vt9^mEC(Bu9Zz5WFtbpy#hMMX2Xepy$4>aixd zt=e!}4-14hUOFo4;a7W1XYS!SxYTFG&axym)(c*3kh|_OL$=|xAn;DP?>+z2U*}O@ z0n!pwsIG0TWi<>b&Hi%Q?!-@bzoYz^{E5Roa~&D?M^}|aE2Q72<1XrN7aq7sdDR16 z3w;YvAIqdVA& zZxp2wR}1oh_1TNY%LvBSN$J`79fTavwA5d|y>RxRf?syb&dx{dk+%3E_D-fRPsdM9 z=aT}*<-N6ituouRS~U7JcXzH#NLushL)8b~4<4ONO58^m{Pt8u)z05KYlgbb9mgk5 zZAYk_*(OTZGSoxD*Afk)5rQ!amesAChC=8HWB4(zeBetL2~U@8MZo0qf%E_;*eVO$ zf?L#44K!;z5uewnw`h`+wwNh^hiy9U{_!6(l&W-wXttX)b=Efycf){_xWVmVrun&JNGo?tk!d%PwNbh70or8rfqC*++~ohzV3MN zuCrCTlACs&S2;Fo>ba^<{wrVF=x=3|xwql?;jeiz|Nm|NIRE(X!|L*NzuUvh?d5rE z7r)vtG_KX?B-m(9~#_N)8+F|2-DY(5`>= zV$Jq%v33)CHpPAuy%GN4cT{6~J_C=H8DkyGgN6TJTHN6Ild>_NDcP{>X842SyO{Jh zF%}ekpU=W~pY=gsbHCVcHUqK5>9wqNYqUQkNgwF{%JO|D$fSg7*0B9AZ@ImnS^DrFk)mhzyn+D;JYD@<);T3K F0RS)aY Date: Wed, 20 Mar 2013 19:47:48 +0100 Subject: [PATCH 015/127] Optimize PNGs --- couchpotato/static/images/couch.png | Bin 59075 -> 59047 bytes couchpotato/static/images/gear.png | Bin 1196 -> 1104 bytes couchpotato/static/images/icon.download.png | Bin 306 -> 304 bytes couchpotato/static/images/icon.readd.png | Bin 778 -> 604 bytes .../static/images/notify.couch.large.png | Bin 96881 -> 54128 bytes .../static/images/notify.couch.medium.png | Bin 33804 -> 19516 bytes couchpotato/static/images/sprite.png | Bin 1889 -> 1795 bytes 7 files changed, 0 insertions(+), 0 deletions(-) diff --git a/couchpotato/static/images/couch.png b/couchpotato/static/images/couch.png index 3bc445e77a44115918249238cec208db12b5dc53..5b7efc706cd3f4673d76b5ff6eae6f0fa9ce8f0a 100644 GIT binary patch literal 59047 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 literal 59075 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 diff --git a/couchpotato/static/images/gear.png b/couchpotato/static/images/gear.png index f1d63badebeb9080b4daa052a673f24a1889b28b..d2aad53c310a4bee957d770765f44ed5efe7620f 100644 GIT binary patch delta 1084 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 diff --git a/couchpotato/static/images/icon.download.png b/couchpotato/static/images/icon.download.png index e64e999707fdb5f44ffcf5984e9026d12eea602b..d8283918ed877fcf78b1df6891c16106c418d165 100644 GIT binary patch delta 146 zcmV;D0B!%W0m{%bb-89<4= z5+DMe&bbp47`^vn3X9fyNud!!G%2)F%Ju{L=#)|&{RF^%9gH!<+GBtU=gC?-%~%D< z3*7c=->}F70Lk$j)bWMo=8nL^{Q*l&P4Kf9=Nttx0WNT701E&B07*qoM6N<$g0w_J A<^TWy delta 148 zcmV;F0BirS0-rhj)yL_t(|UTw|E5`!QVMA1CNG&9b#N&f#U<0}e_g?HCy)31k! zBsfWw0cQx|L;+)raV%l6)-EM9N~xxVR%_kfAc4*p(*ZI8`gL&54Qr1CHRj2CKh3BU zhzi^O**8=o0bO$Zj%()^mM`xNEWB@Ec?)8A=nv-{1u~5+a}BBh0000*TsLnbmBqEn6+OHp@yXP_m+0 zILH8tO=sFzDNb;Sx)q7YF#ZH$DttwQK*(hTB1uqIgF&uhBC9zp&mHmhOWZnW-a54P zZ1477p6A_5{D*8YoZWxOT=$A&k!AU0C-|fsXNFpS6q{--RDT_7+aCh$K{dH7?a1vr zO*}b|yq)cxdJK0e0{2`Pw&W!AF*n-1M&zRThVzV7f%3to*=lQhHg}=X*$Y=Pgvo_C zo_%|Z;iM1R84uJ67gX`5xD&GCl0~{Ya?IF}(nTE5zjnem_W{979FEi!9)9*h6L&!w zwWDKt1UEwlNPog6G>>ahX%a6|snoocK&kp#noPVzba57;%tzRhL6{TIVTzAI8+GAs zn1n1~f^@=&8x#EyjrTw}BFAnGSG^KA*v0)y#$LdieupQY0vL#m!5H;G6?_DNz6~c7 zqHKwxCS7^29^C#G2uV3E4!7cPH~;6dzJb^)IX6EL8GnU+CII7mH~PXhXx})%QHoKh zVB3DbN}*8l3VPW3QYm+dt!x6LqZav!)0c@|4W}|{KMC`+2g;xo-M&He`K>5#=7Fka zmJn;$tg2uYU$0s^ev6BOwhBM7U3m0+-B2f_?)zvR)j(`lLPU0O;U=Bf%X+kE!#>~ilNK8V%Skqg!AV>;KQ>WN+p(chH zhHopw`d}))N2HrhDKV8&3ZtAW%WS&Mz1@4a-FtJc4|d==2cE1kOzk?T2Zza1ZGrSk!h50W(Tg_1Tj?};Y`G~F5M^It^MLRMS8_jFIkrieSv zP1P!D>J?Rbk=CV?s)RUs72DDY;wsj+%4=Rm>=vR;E#U)ZEo~as-5L=#sv<63pXkae zi0+}98&3YYO$q|XC}~8As~iv8gV%YWItygB=h5e&1QkYW`b>p4ctx_Fh~G5<^;E$0JakW9LK>74hUg! z!y{|X-}7*GjHY){Ov36wfTX}+H-Q!p!?wm?2lY*2)HRJ*}BcCYz^_ z)J1&qFF1T4m=)+|aJkj#*}EvzN&Gyr*jy+L6Uxa+^M*mD?9+`Wx}8pi-aebd5iF~* z$yC>ZN+jyYxiLQ$?Ovgc3YVx-PE2Ts?Ly=qh1`l+tdA)UGme!^nSW_dDMuq$5_8M$ zsH$4q6=IvwH2r&Ht!&xYUd*c?)aCZZDuNUgdF0VF+Em+L8a=7u*Y%orzMi9_$Sbc3 z=Xjcjqq1YK-(>cau@p<4>K$uT{5KRi;VtZ30?#h*Sz87SWF8F`FQm#m_mVRiX}zg= GcmD!)?tHBP diff --git a/couchpotato/static/images/notify.couch.large.png b/couchpotato/static/images/notify.couch.large.png index e64347bd4ff9f63cac2c781c87e8139f8b3cb7ba..e918c0493a0aa8515a4c7dcf8546486543550643 100644 GIT binary patch literal 54128 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�c2d&$~6|iUSrGe(xdC+W-a%_iI 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 YUnV4fCS)Dg0sbzW_4>K-r)zirKaGt{Z2$lO diff --git a/couchpotato/static/images/notify.couch.medium.png b/couchpotato/static/images/notify.couch.medium.png index c02974dea53917bdcc188b6132c630425c54d22d..939aca406d2343fa5398fa0222c11f1625c51331 100644 GIT binary patch literal 19516 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

)lpLUyZ|HWP26c-ymXzO6)24`NJ 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 z

l^**UFqGF?v$%}6wvHN`yKwxAf+VZp1V6?B0KD*W~|w(0XMkcvEsv? z_}|j=nov4#iYSGA4e==RhK1_ij-B9)Ilh-A^IWVpCq4{Al8L>v`ggXASjuCp?K{k} zqOECn^2GGJ&|9X6hBh71vcAEVo>w(aG1Kufxk$KlhU`(WSiE&{t7j-pR0Fl*S>U~z)fT-RltanG zBu`bmL%;vLySL3PJ>Cr{K9IlpFIS~a^90^|C5ObK)wKNllv~>Ysj|G;XzM|JG;xYL zKcB9KH(eZ&cK&PW@VgPKGYgM=u)qeW_$*PsXO!WCavrb88 zM|3Fi*1{o(CpPQ=W*c!lB{|<1dFc9hfWOL6=7xiqXUrxEnxZj=JBgQ#)v6H=KAti- z=82;FsK=$yK>l_V@uKRT=$VP$Tpvh4f8|OxBb(~|z*WE;;3&dzTh?L9sJ}Zu(o8#w z(R7i7>jG_F;g#f^Nar&{oGQyZES2kb-^>7bhPHe;NXQP$FsAP_QC??`Mr?b46J5ND z^J=(v+T{Qsm zbF~@2N#VRq-1Q}`4z!T$%*@GhP-lpIl3L=LE#tWw{wRzaJ7uf#hEaYRYV2}$XP%!7 z<%36)D@)%UONhkiE>%%@hIqIKW9`0mb-_Z>b#DZmB~B@DmjyQ^3KEx%f0do%FjF_) zVApS1AlPJ_#<98T7K#YUOJ&ZF=>$vt9^mEC(Bu9Zz5WFtbpy#hMMX2Xepy$4>aixd zt=e!}4-14hUOFo4;a7W1XYS!SxYTFG&axym)(c*3kh|_OL$=|xAn;DP?>+z2U*}O@ z0n!pwsIG0TWi<>b&Hi%Q?!-@bzoYz^{E5Roa~&D?M^}|aE2Q72<1XrN7aq7sdDR16 z3w;YvAIqdVA& zZxp2wR}1oh_1TNY%LvBSN$J`79fTavwA5d|y>RxRf?syb&dx{dk+%3E_D-fRPsdM9 z=aT}*<-N6ituouRS~U7JcXzH#NLushL)8b~4<4ONO58^m{Pt8u)z05KYlgbb9mgk5 zZAYk_*(OTZGSoxD*Afk)5rQ!amesAChC=8HWB4(zeBetL2~U@8MZo0qf%E_;*eVO$ zf?L#44K!;z5uewnw`h`+wwNh^hiy9U{_!6(l&W-wXttX)b=Efycf){_xWVmVrun&JNGo?tk!d%PwNbh70or8rfqC*++~ohzV3MN zuCrCTlACs&S2;Fo>ba^<{wrVF=x=3|xwql?;jeiz|Nm|NIRE(X!|L*NzuUvh?d5rE z7r)vtG_KX?B-m(9~#_N)8+F|2-DY(5`>= zV$Jq%v33)CHpPAuy%GN4cT{6~J_C=H8DkyGgN6TJTHN6Ild>_NDcP{>X842SyO{Jh zF%}ekpU=W~pY=gsbHCVcHUqK5>9wqNYqUQkNgwF{%JO|D$fSg7*0B9AZ@ImnS^DrFk)mhzyn+D;JYD@<);T3K F0RS)aYX-lCLsD)BmdIMH2 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 Date: Wed, 20 Mar 2013 20:30:34 +0100 Subject: [PATCH 016/127] Force pushalot image refresh --- couchpotato/core/notifications/pushalot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/notifications/pushalot/main.py b/couchpotato/core/notifications/pushalot/main.py index 1c82a25a..c35efa5c 100644 --- a/couchpotato/core/notifications/pushalot/main.py +++ b/couchpotato/core/notifications/pushalot/main.py @@ -20,7 +20,7 @@ class Pushalot(Notification): 'Body': toUnicode(message), 'IsImportant': self.conf('important'), 'IsSilent': self.conf('silent'), - 'Image': toUnicode(self.getNotificationImage('medium')), + 'Image': toUnicode(self.getNotificationImage('medium') + '?1'), 'Source': toUnicode(self.default_title) } From 2c64641a1b41b1c1079d6a5e9f7540652bdd6d33 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 20 Mar 2013 22:45:31 +0100 Subject: [PATCH 017/127] Prepend lists when merging event objects --- couchpotato/core/event.py | 2 +- couchpotato/core/helpers/variable.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index bd704f5c..19e97541 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -110,7 +110,7 @@ def fireEvent(name, *args, **kwargs): if isinstance(results[0], dict): merged = {} for result in results: - merged = mergeDicts(merged, result) + merged = mergeDicts(merged, result, prepend_list = True) results = merged # Lists diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 82bf88f7..ff1043bb 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -53,7 +53,7 @@ def getDataDir(): def isDict(object): return isinstance(object, dict) -def mergeDicts(a, b): +def mergeDicts(a, b, prepend_list = False): assert isDict(a), isDict(b) dst = a.copy() @@ -67,7 +67,7 @@ def mergeDicts(a, b): if isDict(current_src[key]) and isDict(current_dst[key]): stack.append((current_dst[key], current_src[key])) elif isinstance(current_src[key], list) and isinstance(current_dst[key], list): - current_dst[key].extend(current_src[key]) + current_dst[key] = current_src[key] + current_dst[key] if prepend_list else current_dst[key] + current_src[key] current_dst[key] = removeListDuplicates(current_dst[key]) else: current_dst[key] = current_src[key] From 19641bd8979d7f215ffceb8efb3467fe39cf0a9a Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 20 Mar 2013 22:47:14 +0100 Subject: [PATCH 018/127] Give the scanner some rest when to many threads --- couchpotato/core/plugins/scanner/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index b822bc0f..fd43075a 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -11,6 +11,7 @@ from subliminal.videos import Video import enzyme import os import re +import threading import time import traceback @@ -388,6 +389,11 @@ class Scanner(Plugin): if on_found: on_found(group, total_found, total_found - len(processed_movies)) + # Wait for all the async events calm down a bit + while threading.activeCount() > 100 and not self.shuttingDown(): + log.debug('Too many threads active, waiting a few seconds') + time.sleep(10) + if len(processed_movies) > 0: log.info('Found %s movies in the folder %s', (len(processed_movies), folder)) else: From de36faa0a7a2110c629fc569a79f41db71e8d873 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 22 Mar 2013 21:59:28 +0100 Subject: [PATCH 019/127] DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS DEVELOPERS --- couchpotato/core/_base/_core/__init__.py | 2 +- couchpotato/runner.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index c8c3fda6..4d1a6840 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -70,7 +70,7 @@ config = [{ 'name': 'development', 'default': 0, 'type': 'bool', - 'description': 'Disables some checks/downloads for faster reloading.', + 'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.', }, { 'name': 'data_dir', diff --git a/couchpotato/runner.py b/couchpotato/runner.py index de538485..0ae14ce0 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -190,9 +190,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En version_control(db, repo, version = latest_db_version) current_db_version = db_version(db, repo) - if current_db_version < latest_db_version and not development: - log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version)) - upgrade(db, repo) + if current_db_version < latest_db_version: + if development: + log.error('There is a database migration ready, but you are running development mode, so it won\'t be used. If you see this, you are stupid. Please disable development mode.') + else: + log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version)) + upgrade(db, repo) # Configure Database from couchpotato.core.settings.model import setup From a97570027d8dc6ca192a1c910d89537741490b35 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 22 Mar 2013 22:31:49 +0100 Subject: [PATCH 020/127] Prevent null in boolean column. fix #1374 --- couchpotato/core/settings/model.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 141ee4fe..8c2af462 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -76,7 +76,7 @@ class LibraryTitle(Entity): title = Field(Unicode) simple_title = Field(Unicode, index = True) - default = Field(Boolean, index = True) + default = Field(Boolean, default = False, index = True) language = OneToMany('Language') libraries = ManyToOne('Library') @@ -146,7 +146,7 @@ class Quality(Entity): identifier = Field(String(20), unique = True) label = Field(Unicode(20)) - order = Field(Integer, index = True) + order = Field(Integer, default = 0, index = True) size_min = Field(Integer) size_max = Field(Integer) @@ -160,21 +160,27 @@ class Profile(Entity): using_options(order_by = 'order') label = Field(Unicode(50)) - order = Field(Integer, index = True) - core = Field(Boolean) - hide = Field(Boolean) + order = Field(Integer, default = 0, index = True) + core = Field(Boolean, default = False) + hide = Field(Boolean, default = False) movie = OneToMany('Movie') types = OneToMany('ProfileType', cascade = 'all, delete-orphan') + def to_dict(self, deep = {}, exclude = []): + orig_dict = super(Profile, self).to_dict(deep = deep, exclude = exclude) + orig_dict['core'] = orig_dict.get('core') or False + orig_dict['hide'] = orig_dict.get('hide') or False + + return orig_dict class ProfileType(Entity): """""" using_options(order_by = 'order') - order = Field(Integer, index = True) - finish = Field(Boolean) - wait_for = Field(Integer) + order = Field(Integer, default = 0, index = True) + finish = Field(Boolean, default = True) + wait_for = Field(Integer, default = 0) quality = ManyToOne('Quality') profile = ManyToOne('Profile') @@ -185,7 +191,7 @@ class File(Entity): path = Field(Unicode(255), nullable = False, unique = True) part = Field(Integer, default = 1) - available = Field(Boolean) + available = Field(Boolean, default = True) type = ManyToOne('FileType') properties = OneToMany('FileProperty') From f9ace29cabb6c7df6e0464e60b46f3ee98cd81e1 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sat, 23 Mar 2013 00:40:38 +0100 Subject: [PATCH 021/127] Fix the names of quality identifiers 720P should be 720p, 1080P should be 1080p. Ref http://en.wikipedia.org/wiki/720p and http://en.wikipedia.org/wiki/1080p Note that this update only changes anything for new databases. For as far as I can see the change for existing databases is minimal. --- couchpotato/core/plugins/quality/main.py | 4 ++-- couchpotato/core/plugins/renamer/__init__.py | 2 +- couchpotato/core/plugins/trailer/__init__.py | 2 +- couchpotato/core/settings/model.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 4cd5b8de..c32ad81d 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -19,8 +19,8 @@ class QualityPlugin(Plugin): qualities = [ {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, - {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']}, - {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']}, + {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']}, + {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 6ce21922..22277ba5 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -13,7 +13,7 @@ rename_options = { 'thename': 'The Moviename', 'year': 'Year (2011)', 'first': 'First letter (M)', - 'quality': 'Quality (720P)', + 'quality': 'Quality (720p)', 'video': 'Video (x264)', 'audio': 'Audio (DTS)', 'group': 'Releasegroup name', diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py index 282b3482..e4c51bd1 100644 --- a/couchpotato/core/plugins/trailer/__init__.py +++ b/couchpotato/core/plugins/trailer/__init__.py @@ -22,7 +22,7 @@ config = [{ 'name': 'quality', 'default': '720p', 'type': 'dropdown', - 'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')], + 'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')], }, { 'name': 'name', diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 141ee4fe..6a920322 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -141,7 +141,7 @@ class Status(Entity): class Quality(Entity): - """Quality name of a release, DVD, 720P, DVD-Rip etc""" + """Quality name of a release, DVD, 720p, DVD-Rip etc""" using_options(order_by = 'order') identifier = Field(String(20), unique = True) From 1a54d8fad93b061a911514d500428c9aca7509b0 Mon Sep 17 00:00:00 2001 From: Jonas Forsberg Date: Sat, 23 Mar 2013 11:49:40 +0100 Subject: [PATCH 022/127] Letterboxd wishlist importer --- .../automation/letterboxd/__init__.py | 28 ++++++++++++++ .../providers/automation/letterboxd/main.py | 37 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 couchpotato/core/providers/automation/letterboxd/__init__.py create mode 100644 couchpotato/core/providers/automation/letterboxd/main.py diff --git a/couchpotato/core/providers/automation/letterboxd/__init__.py b/couchpotato/core/providers/automation/letterboxd/__init__.py new file mode 100644 index 00000000..17f5ae2f --- /dev/null +++ b/couchpotato/core/providers/automation/letterboxd/__init__.py @@ -0,0 +1,28 @@ +from .main import Letterboxd + +def start(): + return Letterboxd() + +config = [{ + 'name': 'letterboxd', + 'groups': [ + { + 'tab': 'automation', + 'list': 'watchlist_providers', + 'name': 'letterboxd_automation', + 'label': 'Letterboxd', + 'description': 'import movies from your Letterboxd watchlist', + 'options': [ + { + 'name': 'automation_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_username', + 'label': 'Username', + }, + ], + }, + ], +}] \ No newline at end of file diff --git a/couchpotato/core/providers/automation/letterboxd/main.py b/couchpotato/core/providers/automation/letterboxd/main.py new file mode 100644 index 00000000..3c60d58d --- /dev/null +++ b/couchpotato/core/providers/automation/letterboxd/main.py @@ -0,0 +1,37 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.automation.base import Automation +from bs4 import BeautifulSoup +import re + +log = CPLog(__name__) + + +class Letterboxd(Automation): + url = 'http://letterboxd.com/%s/watchlist/' + pattern = re.compile(r'(.*)\((\d*)\)') + + def getIMDBids(self): + + if not self.conf('automation_username'): + log.error('Please fill in your username') + return [] + + movies = [] + + for movie in self.getWatchlist(): + imdb_id = self.search(movie.get('title'), movie.get('year'), imdb_only = True) + movies.append(imdb_id) + + return movies + + def getWatchlist(self): + url = self.url % self.conf('automation_username') + soup = BeautifulSoup(self.getHTMLData(url)) + + movies = [] + + for movie in soup.find_all('a', attrs = { 'class': 'frame' }): + match = filter(None, self.pattern.split(movie['title'])) + movies.append({ 'title': match[0], 'year': match[1] }) + + return movies From b0d556c8eb18a15565ababb435e9e61dbca1f36d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 24 Mar 2013 19:54:11 +0100 Subject: [PATCH 023/127] Try and find release status by ID instead of name. closes #1511 Thanks to @mano3m --- couchpotato/core/downloaders/base.py | 38 +++++++ .../core/downloaders/nzbget/__init__.py | 6 + couchpotato/core/downloaders/nzbget/main.py | 106 +++++++++++++++++- .../core/downloaders/nzbvortex/main.py | 8 +- couchpotato/core/downloaders/sabnzbd/main.py | 44 ++++---- .../core/downloaders/transmission/main.py | 2 +- couchpotato/core/downloaders/utorrent/main.py | 6 +- couchpotato/core/plugins/renamer/main.py | 10 +- couchpotato/core/plugins/searcher/main.py | 17 ++- 9 files changed, 200 insertions(+), 37 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 70500dc0..69449249 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -1,5 +1,6 @@ from base64 import b32decode, b16encode from couchpotato.core.event import addEvent +from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import Provider import random @@ -103,6 +104,12 @@ class Downloader(Provider): log.error('Failed converting magnet url to torrent: %s', (torrent_hash)) return False + def downloadReturnId(self, download_id): + return { + 'downloader': self.getName(), + 'id': download_id + } + def isDisabled(self, manual, data): return not self.isEnabled(manual, data) @@ -116,3 +123,34 @@ class Downloader(Provider): return super(Downloader, self).isEnabled() and \ ((d_manual and manual) or (d_manual is False)) and \ (not data or self.isCorrectType(data.get('type'))) + + +class StatusList(list): + + provider = None + + def __init__(self, provider, **kwargs): + + self.provider = provider + self.kwargs = kwargs + + super(StatusList, self).__init__() + + def extend(self, results): + for r in results: + self.append(r) + + def append(self, result): + new_result = self.fillResult(result) + super(StatusList, self).append(new_result) + + def fillResult(self, result): + + defaults = { + 'id': 0, + 'status': 'busy', + 'downloader': self.provider.getName(), + } + + return mergeDicts(defaults, result) + diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 403a7e7d..5349914a 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -48,6 +48,12 @@ config = [{ 'advanced': True, 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', }, + { + 'name': 'delete_failed', + 'default': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, ], } ], diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 82d8a363..b767ef75 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -1,9 +1,11 @@ from base64 import standard_b64encode -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog +from datetime import timedelta import re +import shutil import socket import traceback import xmlrpclib @@ -50,7 +52,107 @@ class NZBGet(Downloader): if xml_response: log.info('NZB sent successfully to NZBGet') - return True + groups = rpc.listgroups() + nzb_id = [item['NZBID'] for item in groups if item['NZBFilename'] == nzb_name][0] + return self.downloadReturnId(nzb_id) else: log.error('NZBGet could not add %s to the queue.', nzb_name) return False + + def getAllDownloadStatus(self): + + log.debug('Checking NZBGet download status.') + + url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} + + rpc = xmlrpclib.ServerProxy(url) + try: + if rpc.writelog('INFO', 'CouchPotato connected to check status'): + log.info('Successfully connected to NZBGet') + else: + log.info('Successfully connected to NZBGet, but unable to send a message') + except socket.error: + log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') + return False + except xmlrpclib.ProtocolError, e: + if e.errcode == 401: + log.error('Password is incorrect.') + else: + log.error('Protocol Error: %s', e) + return False + + # Get NZBGet data + try: + status = rpc.status() + groups = rpc.listgroups() + queue = rpc.postqueue(0) + history = rpc.history() + except: + log.error('Failed getting data: %s', traceback.format_exc(1)) + return False + + statuses = StatusList(self) + + for item in groups: + log.debug('Found %s in NZBGet download queue', item['NZBFilename']) + statuses.append({ + 'id': item['NZBID'], + 'name': item['NZBFilename'], + 'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED', + # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item + 'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1, + }) + + for item in queue: + log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename']) + statuses.append({ + 'id': item['NZBID'], + 'name': item['NZBFilename'], + 'original_status': item['Stage'], + 'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1, + }) + + for item in history: + log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log'])) + statuses.append({ + 'id': item['NZBID'], + 'name': item['NZBFilename'], + 'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed', + 'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'], + 'timeleft': str(timedelta(seconds = 0)), + }) + + return statuses + + def removeFailed(self, item): + + log.info('%s failed downloading, deleting...', item['name']) + + url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} + + rpc = xmlrpclib.ServerProxy(url) + try: + if rpc.writelog('INFO', 'CouchPotato connected to delete some history'): + log.info('Successfully connected to NZBGet') + else: + log.info('Successfully connected to NZBGet, but unable to send a message') + except socket.error: + log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') + return False + except xmlrpclib.ProtocolError, e: + if e.errcode == 401: + log.error('Password is incorrect.') + else: + log.error('Protocol Error: %s', e) + return False + + try: + history = rpc.history() + if rpc.editqueue('HistoryDelete', 0, "", [int(item['id'][len('NZBGet_'):])]): + path = [hist['DestDir'] for hist in history if hist['NZBID'] == item['id']][0] + shutil.rmtree(path, True) + except: + log.error('Failed deleting: %s', traceback.format_exc(0)) + return False + + return True diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index 1462c678..e6cbd027 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -1,5 +1,5 @@ from base64 import b64encode -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import tryUrlencode, ss from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog @@ -29,7 +29,9 @@ class NZBVortex(Downloader): nzb_filename = self.createFileName(data, filedata, movie) self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True) - return True + raw_statuses = self.call('nzb') + nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['name'] == nzb_filename][0] + return self.downloadReturnId(nzb_id) except: log.error('Something went wrong sending the NZB file: %s', traceback.format_exc()) return False @@ -38,7 +40,7 @@ class NZBVortex(Downloader): raw_statuses = self.call('nzb') - statuses = [] + statuses = StatusList(self) for item in raw_statuses.get('nzbs', []): # Check status diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index a287f119..8b374698 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import tryUrlencode, ss from couchpotato.core.helpers.variable import cleanHost, mergeDicts from couchpotato.core.logger import CPLog @@ -17,8 +17,7 @@ class Sabnzbd(Downloader): log.info('Sending "%s" to SABnzbd.', data.get('name')) - params = { - 'apikey': self.conf('api_key'), + req_params = { 'cat': self.conf('category'), 'mode': 'addurl', 'nzbname': self.createNzbName(data, movie), @@ -31,17 +30,15 @@ class Sabnzbd(Downloader): # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb nzb_filename = self.createFileName(data, filedata, movie) - params['mode'] = 'addfile' + req_params['mode'] = 'addfile' else: - params['name'] = data.get('url') - - url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params) + req_params['name'] = data.get('url') try: - if params.get('mode') is 'addfile': - sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) + if req_params.get('mode') is 'addfile': + sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True) else: - sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) + sab_data = self.call(req_params) except URLError: log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0)) return False @@ -49,17 +46,19 @@ class Sabnzbd(Downloader): log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0)) return False - result = sab.strip() - if not result: - log.error('SABnzbd didn\'t return anything.') + if sab_data.get('error'): + log.error('Error getting data from SABNZBd: %s', sab_data.get('error')) return False - log.debug('Result text from SAB: %s', result[:40]) - if result[:2] == 'ok': + log.debug('Result from SAB: %s', sab_data) + if sab_data.get('status'): log.info('NZB sent to SAB successfully.') - return True + if filedata: + return self.downloadReturnId(sab_data.get('nzo_ids')[0]) + else: + return True else: - log.error(result[:40]) + log.error(sab_data) return False def getAllDownloadStatus(self): @@ -85,14 +84,13 @@ class Sabnzbd(Downloader): log.error('Failed getting history json: %s', traceback.format_exc(1)) return False - statuses = [] + statuses = StatusList(self) # Get busy releases for item in queue.get('slots', []): statuses.append({ 'id': item['nzo_id'], 'name': item['filename'], - 'status': 'busy', 'original_status': item['status'], 'timeleft': item['timeleft'] if not queue['paused'] else -1, }) @@ -133,21 +131,21 @@ class Sabnzbd(Downloader): return True - def call(self, params, use_json = True): + def call(self, request_params, use_json = True, **kwargs): - url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, { + url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, { 'apikey': self.conf('api_key'), 'output': 'json' })) - data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) + data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs) if use_json: d = json.loads(data) if d.get('error'): log.error('Error getting data from SABNZBd: %s', d.get('error')) return {} - return d[params['mode']] + return d.get(request_params['mode']) or d else: return data diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 5c13af6e..6c4607fb 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -62,7 +62,7 @@ class Transmission(Downloader): if torrent_params: trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params) - return True + return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) except Exception, err: log.error('Failed to change settings for transfer: %s', err) return False diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index 5953b117..c64db135 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -1,6 +1,6 @@ from base64 import b16encode, b32decode from bencode import bencode, bdecode -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import isInt, ss from couchpotato.core.logger import CPLog from hashlib import sha1 @@ -66,7 +66,7 @@ class uTorrent(Downloader): self.utorrent_api.set_torrent(torrent_hash, torrent_params) if self.conf('paused', default = 0): self.utorrent_api.pause_torrent(torrent_hash) - return True + return self.downloadReturnId(torrent_hash) except Exception, err: log.error('Failed to send torrent to uTorrent: %s', err) return False @@ -103,7 +103,7 @@ class uTorrent(Downloader): log.debug('Nothing in queue') return False - statuses = [] + statuses = StatusList(self) # Get torrents for item in queue.get('torrents', []): diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index b7062157..aba20724 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -571,8 +571,16 @@ class Renamer(Plugin): found = False for item in statuses: - if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']: + found_release = False + if rel_dict['info'].get('download_id'): + if item['id'] == rel_dict['info']['download_id'] and item['downloader'] == rel_dict['info']['download_downloader']: + log.debug('Found release by id: %s', item['id']) + found_release = True + else: + if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']: + found_release = True + if found_release: timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft'] log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft)) diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index b4c4937a..a34b0b47 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -285,10 +285,10 @@ class Searcher(Plugin): if filedata == 'try_next': return filedata - successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) - - if successful: + download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) + log.debug('Downloader result: %s', download_result) + if download_result: try: # Mark release as snatched db = get_session() @@ -298,6 +298,15 @@ class Searcher(Plugin): done_status = fireEvent('status.get', 'done', single = True) rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id') + + # Save download-id info if returned + if isinstance(download_result, dict): + for key in download_result: + rls_info = ReleaseInfo( + identifier = 'download_%s' % key, + value = toUnicode(download_result.get(key)) + ) + rls.info.append(rls_info) db.commit() log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) @@ -333,7 +342,7 @@ class Searcher(Plugin): return True - log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', ''))) + log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', ''))) return False From 72ba1a173cc7513def1e29b6d96566e15adc015d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 24 Mar 2013 20:18:49 +0100 Subject: [PATCH 024/127] Make letterboxd multi-watchlist --- .../automation/letterboxd/__init__.py | 12 ++++++-- .../providers/automation/letterboxd/main.py | 28 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/providers/automation/letterboxd/__init__.py b/couchpotato/core/providers/automation/letterboxd/__init__.py index 17f5ae2f..f2b8486b 100644 --- a/couchpotato/core/providers/automation/letterboxd/__init__.py +++ b/couchpotato/core/providers/automation/letterboxd/__init__.py @@ -11,7 +11,7 @@ config = [{ 'list': 'watchlist_providers', 'name': 'letterboxd_automation', 'label': 'Letterboxd', - 'description': 'import movies from your Letterboxd watchlist', + 'description': 'Import movies from any public Letterboxd watchlist', 'options': [ { 'name': 'automation_enabled', @@ -19,10 +19,16 @@ config = [{ 'type': 'enabler', }, { - 'name': 'automation_username', + 'name': 'automation_urls_use', + 'label': 'Use', + }, + { + 'name': 'automation_urls', 'label': 'Username', + 'type': 'combined', + 'combine': ['automation_urls_use', 'automation_urls'], }, ], }, ], -}] \ No newline at end of file +}] diff --git a/couchpotato/core/providers/automation/letterboxd/main.py b/couchpotato/core/providers/automation/letterboxd/main.py index 3c60d58d..7bae2ad5 100644 --- a/couchpotato/core/providers/automation/letterboxd/main.py +++ b/couchpotato/core/providers/automation/letterboxd/main.py @@ -1,19 +1,22 @@ +from bs4 import BeautifulSoup +from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation -from bs4 import BeautifulSoup import re log = CPLog(__name__) class Letterboxd(Automation): + url = 'http://letterboxd.com/%s/watchlist/' pattern = re.compile(r'(.*)\((\d*)\)') def getIMDBids(self): - if not self.conf('automation_username'): - log.error('Please fill in your username') + urls = splitString(self.conf('automation_urls')) + + if len(urls) == 0: return [] movies = [] @@ -25,13 +28,22 @@ class Letterboxd(Automation): return movies def getWatchlist(self): - url = self.url % self.conf('automation_username') - soup = BeautifulSoup(self.getHTMLData(url)) + enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))] + urls = splitString(self.conf('automation_urls')) + + index = -1 movies = [] + for username in urls: - for movie in soup.find_all('a', attrs = { 'class': 'frame' }): - match = filter(None, self.pattern.split(movie['title'])) - movies.append({ 'title': match[0], 'year': match[1] }) + index += 1 + if not enablers[index]: + continue + + soup = BeautifulSoup(self.getHTMLData(self.url % username)) + + for movie in soup.find_all('a', attrs = { 'class': 'frame' }): + match = filter(None, self.pattern.split(movie['title'])) + movies.append({'title': match[0], 'year': match[1] }) return movies From 67612fce986449462686747ee0183556b62cf262 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 24 Mar 2013 20:27:38 +0100 Subject: [PATCH 025/127] Use label on combined setting --- couchpotato/static/scripts/page/settings.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index ddb2abf1..6e563bff 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -297,7 +297,7 @@ Page.Settings = new Class({ return group_el }, - + createList: function(content_container){ return new Element('div.option_list').grab( new Element('h3', { @@ -1283,6 +1283,7 @@ Option.Combined = new Class({ self.values = {} self.inputs = {} self.items = [] + self.labels = {} self.options.combine.each(function(name){ @@ -1302,9 +1303,10 @@ Option.Combined = new Class({ var head = new Element('div.head').inject(self.combined_list) Object.each(self.inputs, function(input, name){ + self.labels[name] = input.getPrevious().get('text') new Element('abbr', { 'class': name, - 'text': input.getPrevious().get('text'), + 'text': self.labels[name], //'title': input.getNext().get('text') }).inject(head) }) @@ -1367,7 +1369,7 @@ Option.Combined = new Class({ value_count++; new Element('input[type=text].inlay.'+name, { 'value': value, - 'placeholder': name, + 'placeholder': self.labels[name] || name, 'events': { 'keyup': self.saveCombined.bind(self), 'change': self.saveCombined.bind(self) From b216589e889b4d3118043e6de3292a339542fa96 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 25 Mar 2013 09:58:39 +0100 Subject: [PATCH 026/127] Use normal ID, not extended. fix #1543 --- couchpotato/core/downloaders/nzbget/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index b767ef75..1bc54fd5 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -148,7 +148,7 @@ class NZBGet(Downloader): try: history = rpc.history() - if rpc.editqueue('HistoryDelete', 0, "", [int(item['id'][len('NZBGet_'):])]): + if rpc.editqueue('HistoryDelete', 0, "", [tryInt(item['id'])]): path = [hist['DestDir'] for hist in history if hist['NZBID'] == item['id']][0] shutil.rmtree(path, True) except: From b82319cb5444ee7cdc6c60603121a0d5ceb857c7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 25 Mar 2013 10:48:59 +0100 Subject: [PATCH 027/127] Use existing Trakt auth settings and other cleanup --- couchpotato/core/notifications/base.py | 6 +++-- .../core/notifications/trakt/__init__.py | 21 ++++------------- couchpotato/core/notifications/trakt/main.py | 23 +++++++++---------- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 2604ef47..8ae4bb86 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -2,13 +2,15 @@ from couchpotato.api import addApiView from couchpotato.core.event import addEvent from couchpotato.core.helpers.request import jsonified from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin +from couchpotato.core.providers.base import Provider from couchpotato.environment import Env log = CPLog(__name__) -class Notification(Plugin): +class Notification(Provider): + + type = 'notification' default_title = Env.get('appname') test_message = 'ZOMG Lazors Pewpewpew!' diff --git a/couchpotato/core/notifications/trakt/__init__.py b/couchpotato/core/notifications/trakt/__init__.py index 5a3ec725..b119736c 100644 --- a/couchpotato/core/notifications/trakt/__init__.py +++ b/couchpotato/core/notifications/trakt/__init__.py @@ -8,9 +8,10 @@ config = [{ 'groups': [ { 'tab': 'notifications', - 'name': 'trakt_notification', + 'list': 'notification_providers', + 'name': 'trakt', 'label': 'Trakt', - 'description': 'add movies to your collection once downloaded', + 'description': 'add movies to your collection once downloaded. Fill in your username and password in the Automation Trakt settings', 'options': [ { 'name': 'notification_enabled', @@ -23,21 +24,7 @@ config = [{ 'default': False, 'type': 'bool', }, - { - 'name': 'automation_api_key', - 'label': 'Apikey', - }, - { - 'name': 'automation_username', - 'label': 'Username', - }, - { - 'name': 'automation_password', - 'label': 'Password', - 'type': 'password', - 'description': 'Required even if your account is unprotected.', - }, ], } ], -}] \ No newline at end of file +}] diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt/main.py index 94fecb11..63c19050 100644 --- a/couchpotato/core/notifications/trakt/main.py +++ b/couchpotato/core/notifications/trakt/main.py @@ -1,14 +1,12 @@ from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification -import json -import urllib2 log = CPLog(__name__) class Trakt(Notification): urls = { - 'base': 'http://api.trakt.tv/', + 'base': 'http://api.trakt.tv/%s', 'library': 'movie/library/%s', 'unwatchlist': 'movie/unwatchlist/%s', } @@ -17,17 +15,17 @@ class Trakt(Notification): def notify(self, message = '', data = {}, listener = None): if self.isDisabled(): return - if not data: return + post_data = { 'username': self.conf('automation_username'), 'password' : self.conf('automation_password'), - 'movies': [ { + 'movies': [{ 'imdb_id': data['library']['identifier'], 'title': data['library']['titles'][0]['title'], 'year': data['library']['year'] - } ] - } - result = False + }] if data else [] + } + result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data) if self.conf('remove_watchlist_enabled'): result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data) @@ -35,14 +33,15 @@ class Trakt(Notification): return result def call(self, method_url, post_data): - log.info('opening url: ' + self.urls['base'] + method_url + ', post: ' + str(post_data)) + try: - response = urllib2.urlopen(self.urls['base'] + method_url, json.dumps(post_data)) + response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1) if response: - if json.load(response).get('status') == "success": + if response.get('status') == "success": log.info('Successfully called Trakt') return True except: pass + log.error('Failed to call trakt, check your login.') - return False \ No newline at end of file + return False From 7c234ab7e93f24dacd5ab5610314af34e1dee299 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 25 Mar 2013 11:01:31 +0100 Subject: [PATCH 028/127] Simplify notification providers --- couchpotato/core/notifications/base.py | 10 +++++++--- couchpotato/core/notifications/boxcar/main.py | 1 - couchpotato/core/notifications/email/main.py | 2 -- couchpotato/core/notifications/growl/main.py | 1 - couchpotato/core/notifications/notifo/main.py | 1 - couchpotato/core/notifications/notifymyandroid/main.py | 1 - couchpotato/core/notifications/notifymywp/main.py | 1 - couchpotato/core/notifications/plex/main.py | 1 - couchpotato/core/notifications/prowl/main.py | 1 - couchpotato/core/notifications/pushalot/main.py | 1 - couchpotato/core/notifications/pushover/main.py | 1 - couchpotato/core/notifications/toasty/main.py | 1 - couchpotato/core/notifications/trakt/main.py | 1 - couchpotato/core/notifications/twitter/main.py | 1 - couchpotato/core/notifications/xbmc/main.py | 1 - 15 files changed, 7 insertions(+), 18 deletions(-) diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 8ae4bb86..dda3dade 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -22,7 +22,7 @@ class Notification(Provider): dont_listen_to = [] def __init__(self): - addEvent('notify.%s' % self.getName().lower(), self.notify) + addEvent('notify.%s' % self.getName().lower(), self._notify) addApiView(self.testNotifyName(), self.test) @@ -35,13 +35,17 @@ class Notification(Provider): def notify(message = None, group = {}, data = None): if not self.conf('on_snatch', default = True) and listener == 'movie.snatched': return - return self.notify(message = message, data = data if data else group, listener = listener) + return self._notify(message = message, data = data if data else group, listener = listener) return notify def getNotificationImage(self, size = 'small'): return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size + def _notify(self, *args, **kwargs): + if self.isEnabled(): + self.notify(*args, **kwargs) + def notify(self, message = '', data = {}, listener = None): pass @@ -51,7 +55,7 @@ class Notification(Provider): log.info('Sending test to %s', test_type) - success = self.notify( + success = self._notify( message = self.test_message, data = {}, listener = 'test' diff --git a/couchpotato/core/notifications/boxcar/main.py b/couchpotato/core/notifications/boxcar/main.py index 3135614a..b30d487a 100644 --- a/couchpotato/core/notifications/boxcar/main.py +++ b/couchpotato/core/notifications/boxcar/main.py @@ -11,7 +11,6 @@ class Boxcar(Notification): url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications' def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return try: message = message.strip() diff --git a/couchpotato/core/notifications/email/main.py b/couchpotato/core/notifications/email/main.py index 014b5856..21fcf157 100644 --- a/couchpotato/core/notifications/email/main.py +++ b/couchpotato/core/notifications/email/main.py @@ -12,7 +12,6 @@ log = CPLog(__name__) class Email(Notification): def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return # Extract all the settings from settings from_address = self.conf('from') @@ -50,6 +49,5 @@ class Email(Notification): return True except: log.error('E-mail failed: %s', traceback.format_exc()) - return False return False diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py index 7f1398d9..caad661b 100644 --- a/couchpotato/core/notifications/growl/main.py +++ b/couchpotato/core/notifications/growl/main.py @@ -44,7 +44,6 @@ class Growl(Notification): log.error('Failed register of growl: %s', traceback.format_exc()) def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return self.register() diff --git a/couchpotato/core/notifications/notifo/main.py b/couchpotato/core/notifications/notifo/main.py index 64eee5ec..312055a8 100644 --- a/couchpotato/core/notifications/notifo/main.py +++ b/couchpotato/core/notifications/notifo/main.py @@ -13,7 +13,6 @@ class Notifo(Notification): url = 'https://api.notifo.com/v1/send_notification' def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return try: params = { diff --git a/couchpotato/core/notifications/notifymyandroid/main.py b/couchpotato/core/notifications/notifymyandroid/main.py index cc6ef662..2c4ac90c 100644 --- a/couchpotato/core/notifications/notifymyandroid/main.py +++ b/couchpotato/core/notifications/notifymyandroid/main.py @@ -9,7 +9,6 @@ log = CPLog(__name__) class NotifyMyAndroid(Notification): def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return nma = pynma.PyNMA() keys = splitString(self.conf('api_key')) diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py index 3258e85f..167b6eeb 100644 --- a/couchpotato/core/notifications/notifymywp/main.py +++ b/couchpotato/core/notifications/notifymywp/main.py @@ -9,7 +9,6 @@ log = CPLog(__name__) class NotifyMyWP(Notification): def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return keys = splitString(self.conf('api_key')) p = PyNMWP(keys, self.conf('dev_key')) diff --git a/couchpotato/core/notifications/plex/main.py b/couchpotato/core/notifications/plex/main.py index 9790ccfe..95139eed 100644 --- a/couchpotato/core/notifications/plex/main.py +++ b/couchpotato/core/notifications/plex/main.py @@ -46,7 +46,6 @@ class Plex(Notification): return True def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")] successful = 0 diff --git a/couchpotato/core/notifications/prowl/main.py b/couchpotato/core/notifications/prowl/main.py index 0990d0d1..e5c4678b 100644 --- a/couchpotato/core/notifications/prowl/main.py +++ b/couchpotato/core/notifications/prowl/main.py @@ -13,7 +13,6 @@ class Prowl(Notification): } def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return data = { 'apikey': self.conf('api_key'), diff --git a/couchpotato/core/notifications/pushalot/main.py b/couchpotato/core/notifications/pushalot/main.py index c35efa5c..4c5e76c3 100644 --- a/couchpotato/core/notifications/pushalot/main.py +++ b/couchpotato/core/notifications/pushalot/main.py @@ -12,7 +12,6 @@ class Pushalot(Notification): } def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return data = { 'AuthorizationToken': self.conf('auth_token'), diff --git a/couchpotato/core/notifications/pushover/main.py b/couchpotato/core/notifications/pushover/main.py index bcd3245c..8df4ed1f 100644 --- a/couchpotato/core/notifications/pushover/main.py +++ b/couchpotato/core/notifications/pushover/main.py @@ -11,7 +11,6 @@ class Pushover(Notification): app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy' def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return http_handler = HTTPSConnection("api.pushover.net:443") diff --git a/couchpotato/core/notifications/toasty/main.py b/couchpotato/core/notifications/toasty/main.py index 638c75dd..79b021e2 100644 --- a/couchpotato/core/notifications/toasty/main.py +++ b/couchpotato/core/notifications/toasty/main.py @@ -12,7 +12,6 @@ class Toasty(Notification): } def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return data = { 'title': self.default_title, diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt/main.py index 63c19050..86d47086 100644 --- a/couchpotato/core/notifications/trakt/main.py +++ b/couchpotato/core/notifications/trakt/main.py @@ -14,7 +14,6 @@ class Trakt(Notification): listen_to = ['movie.downloaded'] def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return post_data = { 'username': self.conf('automation_username'), diff --git a/couchpotato/core/notifications/twitter/main.py b/couchpotato/core/notifications/twitter/main.py index bbb9262b..2db19743 100644 --- a/couchpotato/core/notifications/twitter/main.py +++ b/couchpotato/core/notifications/twitter/main.py @@ -32,7 +32,6 @@ class Twitter(Notification): addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials) def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret')) diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 0187a833..84b9f7e4 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -15,7 +15,6 @@ class XBMC(Notification): use_json_notifications = {} def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return hosts = splitString(self.conf('host')) From 3c0edc0d6a7f9f99f5bfd9c9815e339a21f6671c Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 25 Mar 2013 12:09:32 +0100 Subject: [PATCH 029/127] Clean out more words before search. Thanks to @jkaberg --- couchpotato/core/plugins/scanner/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index fd43075a..b2ac938c 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -75,7 +75,7 @@ class Scanner(Plugin): 'hdtv': ['hdtv'] } - clean = '[ _\,\.\(\)\[\]\-](french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)' + clean = '[ _\,\.\(\)\[\]\-](extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)' multipart_regex = [ '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 '[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1 From 730718a396685d82ff23b7d7f785ad166298f112 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 25 Mar 2013 12:24:54 +0100 Subject: [PATCH 030/127] Link to country codes for subtitles --- couchpotato/core/plugins/subtitle/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py index 686d385e..bbd40853 100644 --- a/couchpotato/core/plugins/subtitle/__init__.py +++ b/couchpotato/core/plugins/subtitle/__init__.py @@ -20,7 +20,7 @@ config = [{ }, { 'name': 'languages', - 'description': 'Comma separated, 2 letter country code. Example: en, nl', + 'description': 'Comma separated, 2 letter country code. Example: en, nl. See the codes at on Wikipedia', }, # { # 'name': 'automatic', From 59b924efe7dab29d4c2976bbf2168f08d396209f Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 25 Mar 2013 22:53:48 +0100 Subject: [PATCH 031/127] Tweak button styling --- couchpotato/static/style/main.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 3ad502b9..91bd1524 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -134,10 +134,9 @@ body > .spinner, .mask{ text-decoration: none; font-weight: bold; line-height: 1; - border-radius: 5px; - box-shadow: 0 1px 3px rgba(0,0,0,0.5); + border-radius: 2px; + box-shadow: 0 1px 2px rgba(0,0,0,0.3); text-shadow: 0 -1px 1px rgba(0,0,0,0.25); - border-bottom: 1px solid rgba(0,0,0,0.25); cursor: pointer; } .button.red { background-color: #ff0000; } From b91a077c91c034613a83968dc69ba1a0d4c35967 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 25 Mar 2013 22:54:22 +0100 Subject: [PATCH 032/127] Easier "get next best release" buttons --- .../plugins/movie/static/movie.actions.js | 93 +++++++++++++++---- .../core/plugins/movie/static/movie.css | 35 +++++-- couchpotato/core/plugins/release/main.py | 16 +++- couchpotato/static/scripts/page/home.js | 2 +- 4 files changed, 119 insertions(+), 27 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/movie.actions.js b/couchpotato/core/plugins/movie/static/movie.actions.js index a56e9abd..2b7f76d0 100644 --- a/couchpotato/core/plugins/movie/static/movie.actions.js +++ b/couchpotato/core/plugins/movie/static/movie.actions.js @@ -94,36 +94,30 @@ MA.Release = new Class({ } else { - var buttons_done = false; + var releases = self.movie.data.releases.sortBy('-info.score'); - self.movie.data.releases.sortBy('-info.score').each(function(release){ - if(buttons_done) return; - - var status = Status.get(release.status_id); + for(x in releases){ + var release = releases[x], + status = Status.get(release.status_id); if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){ - self.hide_on_click = false; - self.show(); - buttons_done = true; + self.showHelper(); + break; } - - }); - + } } }, - show: function(e){ + createReleases: function(){ var self = this; - if(e) - (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( self.release_container = new Element('div.releases.table').adopt( self.trynext_container = new Element('div.buttons.try_container') ) - ).inject(self.movie, 'top'); + ); // Header new Element('div.item.head').adopt( @@ -238,9 +232,71 @@ MA.Release = new Class({ } + }, + + show: function(e){ + var self = this; + if(e) + (e).preventDefault(); + + self.createReleases(); + self.options_container.inject(self.movie, 'top'); self.movie.slide('in', self.options_container); }, + showHelper: function(e){ + var self = this; + if(e) + (e).preventDefault(); + + self.createReleases(); + self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container); + + if(self.next_release || self.last_release){ + + self.trynext_container.adopt( + self.next_release ? [new Element('a.icon.readd', { + 'text': self.last_release ? 'Download another release' : 'Download the best release', + 'events': { + 'click': self.tryNextRelease.bind(self) + } + }), + new Element('a.icon.download', { + 'text': 'pick one yourself', + 'events': { + 'click': function(){ + self.movie.quality.fireEvent('click'); + } + } + })] : null, + new Element('a.icon.completed', { + 'text': 'mark this movie done', + 'events': { + 'click': function(){ + Api.request('movie.delete', { + 'data': { + 'id': self.movie.get('id'), + 'delete_from': 'wanted' + }, + 'onComplete': function(){ + var movie = $(self.movie); + movie.set('tween', { + 'duration': 300, + 'onComplete': function(){ + self.movie.destroy() + } + }); + movie.tween('height', 0); + } + }); + } + } + }) + ) + } + + }, + get: function(release, type){ return release.info[type] || 'n/a' }, @@ -251,14 +307,15 @@ MA.Release = new Class({ var release_el = self.release_container.getElement('#release_'+release.id), icon = release_el.getElement('.download.icon'); - icon.addClass('spinner'); + self.movie.busy(true); Api.request('release.download', { 'data': { 'id': release.id }, 'onComplete': function(json){ - icon.removeClass('spinner') + self.movie.busy(false); + if(json.success) icon.addClass('completed'); else @@ -281,6 +338,8 @@ MA.Release = new Class({ tryNextRelease: function(movie_id){ var self = this; + self.createReleases(); + if(self.last_release) self.ignore(self.last_release); diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 65fe5209..dd1bcbd3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -39,7 +39,7 @@ overflow: hidden; width: 100%; height: 180px; - transition: all 0.2s linear; + transition: all 0.6s cubic-bezier(0.9,0,0.1,1); } .movies.list_list .movie:not(.details_view), @@ -95,7 +95,7 @@ width: 938px; box-shadow: none; border: 0; - background: none; + background: #4e5969; } .movies.thumbs_list .data { @@ -322,8 +322,8 @@ right: 10px; } - .movies .data:hover .action { opacity: 0.6; } - .movies .data:hover .action:hover { opacity: 1; } + .movies .movie:hover .action { opacity: 0.6; } + .movies .movie:hover .action:hover { opacity: 1; } .movies.mass_edit_list .data .actions { display: none; } @@ -338,8 +338,8 @@ opacity: 0; } - .movies.list_list .movie:not(.details_view) .data:hover .actions, - .movies.mass_edit_list .data:hover .actions { + .movies.list_list .movie:not(.details_view):hover .actions, + .movies.mass_edit_list .movie:hover .actions { margin: 0; background: #4e5969; top: 2px; @@ -510,6 +510,29 @@ .movies .movie .releases .last_release > :first-child { margin-left: -6px; } + .movies .movie .trynext { + display: inline; + position: absolute; + right: 135px; + z-index: 2; + opacity: 0; + text-shadow: none; + background: #4e5969; + } + .movies .movie:hover .trynext { + opacity: 1; + } + + .movies .movie .trynext a { + background-position: 5px center; + padding: 0 5px 0 25px; + margin-right: 10px; + color: #FFF; + border-radius: 2px; + } + .movies .movie .trynext a:hover { + background-color: #369545; + } .movies .load_more { display: block; diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 02843f85..68bf60a6 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -160,7 +160,9 @@ class Release(Plugin): db = get_session() id = getParam('id') - status_snatched = fireEvent('status.add', 'snatched', single = True) + + snatched_status = fireEvent('status.add', 'snatched', single = True) + done_status = fireEvent('status.get', 'done', single = True) rel = db.query(Relea).filter_by(id = id).first() if rel: @@ -168,6 +170,8 @@ class Release(Plugin): for info in rel.info: item[info.identifier] = info.value + fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name']) + # Get matching provider provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) @@ -182,8 +186,14 @@ class Release(Plugin): }), manual = True, single = True) if success: - rel.status_id = status_snatched.get('id') - db.commit() + db.expunge_all() + rel = db.query(Relea).filter_by(id = id).first() # Get release again + + if rel.status_id != done_status.get('id'): + rel.status_id = snatched_status.get('id') + db.commit() + + fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name']) return jsonified({ 'success': success diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index c1e91162..004f0513 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -23,7 +23,7 @@ Page.Home = new Class({ 'identifier': 'snatched', 'load_more': false, 'view': 'list', - 'actions': [MA.IMDB, MA.Trailer, MA.Files, MA.Release, MA.Edit, MA.Readd, MA.Refresh, MA.Delete], + 'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Refresh, MA.Delete], 'title': 'Snatched & Available', 'on_empty_element': new Element('div'), 'filter': { From 665c84c6deee719001d13825001ae3c36996da80 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 25 Mar 2013 23:00:12 +0100 Subject: [PATCH 033/127] Ignore done_status on re-add. fix #1547 --- couchpotato/core/plugins/movie/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 5ec968c3..0a99fe6a 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -370,6 +370,7 @@ class MoviePlugin(Plugin): status_active = fireEvent('status.add', 'active', single = True) snatched_status = fireEvent('status.add', 'snatched', single = True) ignored_status = fireEvent('status.add', 'ignored', single = True) + done_status = fireEvent('status.add', 'done', single = True) downloaded_status = fireEvent('status.add', 'downloaded', single = True) default_profile = fireEvent('profile.default', single = True) @@ -397,7 +398,7 @@ class MoviePlugin(Plugin): # Clean snatched history for release in m.releases: - if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]: + if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]: if params.get('ignore_previous', False): release.status_id = ignored_status.get('id') else: From 45c8817c62b6a1d481bb9cb829dbda29be95d9f5 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 26 Mar 2013 21:42:07 +0100 Subject: [PATCH 034/127] Always show release helper. fix #1549 --- .../plugins/movie/static/movie.actions.js | 19 +++---------------- .../core/plugins/movie/static/movie.css | 5 +++++ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/movie.actions.js b/couchpotato/core/plugins/movie/static/movie.actions.js index 2b7f76d0..cf30e8a5 100644 --- a/couchpotato/core/plugins/movie/static/movie.actions.js +++ b/couchpotato/core/plugins/movie/static/movie.actions.js @@ -89,23 +89,10 @@ MA.Release = new Class({ } }); - if(self.movie.data.releases.length == 0){ + if(self.movie.data.releases.length == 0) self.el.hide() - } - else { - - var releases = self.movie.data.releases.sortBy('-info.score'); - - for(x in releases){ - var release = releases[x], - status = Status.get(release.status_id); - - if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){ - self.showHelper(); - break; - } - } - } + else + self.showHelper(); }, diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index dd1bcbd3..3e947ddf 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -518,7 +518,12 @@ opacity: 0; text-shadow: none; background: #4e5969; + min-width: 300px; + text-align: right; } + .wanted .movies .movie .trynext { + padding-right: 50px; + } .movies .movie:hover .trynext { opacity: 1; } From 843ff0eabc537f6bf465772545caa9de2a9e29c9 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 26 Mar 2013 22:02:43 +0100 Subject: [PATCH 035/127] Add some default Newznab providers --- couchpotato/core/providers/nzb/newznab/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index 2349590f..625da1ac 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -12,9 +12,10 @@ config = [{ 'list': 'nzb_providers', 'name': 'newznab', 'order': 10, - 'description': 'Enable NewzNab providers such as NZB.su, \ + 'description': 'Enable NewzNab such as NZB.su, \ NZBs.org, DOGnzb.cr, \ - Spotweb or NZBGeek', + Spotweb, NZBGeek, \ + SmackDown, NZBFinder', 'wizard': True, 'options': [ { @@ -23,18 +24,18 @@ config = [{ }, { 'name': 'use', - 'default': '0,0,0,0' + 'default': '0,0,0,0,0,0' }, { 'name': 'host', - 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info', + 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', 'description': 'The hostname of your newznab provider', }, { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', - 'default': '0,0,0,0', + 'default': '0,0,0,0,0,0', 'description': 'Starting score for each release found via this provider.', }, { From 1df05cf34471de4897a9cc81cba6d2fb50e73dee Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 28 Mar 2013 21:51:42 +0100 Subject: [PATCH 036/127] Don't use directory when it's empty. fix #1448 --- .../core/downloaders/transmission/__init__.py | 2 +- .../core/downloaders/transmission/main.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 210a0d9e..702e61aa 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -41,7 +41,7 @@ config = [{ { 'name': 'directory', 'type': 'directory', - 'description': 'Where should Transmission saved the downloaded files?', + 'description': 'Download to this directory. Keep empty for default Transmission download directory.', }, { 'name': 'ratio', diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 6c4607fb..9b71d6b2 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -27,17 +27,19 @@ class Transmission(Downloader): return False # Set parameters for Transmission - folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] - folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) - - # Create the empty folder to download too - self.makeDir(folder_path) - params = { 'paused': self.conf('paused', default = 0), - 'download-dir': folder_path } + if len(self.conf('directory', default = '')) > 0: + folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] + folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) + + # Create the empty folder to download too + self.makeDir(folder_path) + + params['download-dir'] = folder_path + torrent_params = {} if self.conf('ratio'): torrent_params = { From eab9a735a9e3be5a146f2f5667f7886c4061e87c Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 29 Mar 2013 12:39:06 +0100 Subject: [PATCH 037/127] Show real transmission error. --- couchpotato/core/downloaders/transmission/main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 9b71d6b2..2383685c 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -6,6 +6,7 @@ import httplib import json import os.path import re +import traceback import urllib2 log = CPLog(__name__) @@ -60,13 +61,16 @@ class Transmission(Downloader): else: remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params) + if not remote_torrent: + return False + # Change settings of added torrents - if torrent_params: + elif torrent_params: trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params) return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) - except Exception, err: - log.error('Failed to change settings for transfer: %s', err) + except: + log.error('Failed to change settings for transfer: %s', traceback.format_exc()) return False From 4cedccb178e5aae35ff06b6b15a89ad8d5fc8201 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 29 Mar 2013 12:39:15 +0100 Subject: [PATCH 038/127] Move over html template --- couchpotato/templates/_desktop.html | 78 ---------------------------- couchpotato/templates/_mobile.html | 0 couchpotato/templates/index.html | 79 ++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 79 deletions(-) delete mode 100644 couchpotato/templates/_desktop.html delete mode 100644 couchpotato/templates/_mobile.html diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html deleted file mode 100644 index 1d618066..00000000 --- a/couchpotato/templates/_desktop.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %} - {% endfor %} - {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %} - {% endfor %} - - {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %} - {% endfor %} - {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %} - {% endfor %} - - - - - - - - CouchPotato - - - \ No newline at end of file diff --git a/couchpotato/templates/_mobile.html b/couchpotato/templates/_mobile.html deleted file mode 100644 index e69de29b..00000000 diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html index 217d6bfc..1d618066 100644 --- a/couchpotato/templates/index.html +++ b/couchpotato/templates/index.html @@ -1 +1,78 @@ -{% extends "_desktop.html" %} + + + + {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %} + {% endfor %} + {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %} + {% endfor %} + + {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %} + {% endfor %} + {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %} + {% endfor %} + + + + + + + + CouchPotato + + + \ No newline at end of file From 6a18e546ca1232b3880612aeed583a4d7b4d5701 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Mon, 25 Mar 2013 23:35:23 +0100 Subject: [PATCH 039/127] Add and make use of renamer.scanfolder in downloaders This is the next step in closing the loop between the downloaders and CPS. The download_id and folder from the downloader are used to find the downloaded files and start the renamer. This is done by adding an additional API call: renamer.scanfolder. I tested this for SabNZBd only (!) and everything works as expected. I also added transmission with thanks @manusfreedom for setting this up in f1cf0d91da. @manusfreedom, please check if this works as expected. Note that transmission now has a feature which is not in the other torrent providers: it waits until the seed ratio is met and then removes the torrent. I opened a topic in the forum to discuss how we want to deal with torrents: https://couchpota.to/forum/thread-1704.html --- couchpotato/core/downloaders/base.py | 1 + couchpotato/core/downloaders/nzbget/main.py | 1 + .../core/downloaders/nzbvortex/main.py | 3 +- couchpotato/core/downloaders/sabnzbd/main.py | 12 +- .../core/downloaders/transmission/__init__.py | 11 +- .../core/downloaders/transmission/main.py | 115 +++++++++++++++++- couchpotato/core/downloaders/utorrent/main.py | 4 +- couchpotato/core/plugins/renamer/main.py | 69 ++++++++++- couchpotato/core/plugins/scanner/main.py | 55 ++++++--- 9 files changed, 235 insertions(+), 36 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 69449249..8ccc1369 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -150,6 +150,7 @@ class StatusList(list): 'id': 0, 'status': 'busy', 'downloader': self.provider.getName(), + 'folder': '', } return mergeDicts(defaults, result) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 1bc54fd5..d0533d5c 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -120,6 +120,7 @@ class NZBGet(Downloader): 'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed', 'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'], 'timeleft': str(timedelta(seconds = 0)), + 'folder': item['DestDir'] }) return statuses diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index e6cbd027..f1f8acc6 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -55,7 +55,8 @@ class NZBVortex(Downloader): 'name': item['uiTitle'], 'status': status, 'original_status': item['state'], - 'timeleft':-1, + 'timeleft': -1, + 'folder': item['destinationPath'], }) return statuses diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index 8b374698..f2f217a1 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode, ss from couchpotato.core.helpers.variable import cleanHost, mergeDicts from couchpotato.core.logger import CPLog from couchpotato.environment import Env +from datetime import timedelta from urllib2 import URLError import json import traceback @@ -46,19 +47,15 @@ class Sabnzbd(Downloader): log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0)) return False - if sab_data.get('error'): - log.error('Error getting data from SABNZBd: %s', sab_data.get('error')) - return False - log.debug('Result from SAB: %s', sab_data) - if sab_data.get('status'): + if sab_data.get('status') and not sab_data.get('error'): log.info('NZB sent to SAB successfully.') if filedata: return self.downloadReturnId(sab_data.get('nzo_ids')[0]) else: return True else: - log.error(sab_data) + log.error('Error getting data from SABNZBd: %s', sab_data) return False def getAllDownloadStatus(self): @@ -109,7 +106,8 @@ class Sabnzbd(Downloader): 'name': item['name'], 'status': status, 'original_status': item['status'], - 'timeleft': 0, + 'timeleft': str(timedelta(seconds = 0)), + 'folder': item['storage'], }) return statuses diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 210a0d9e..0fd783b8 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -41,15 +41,22 @@ config = [{ { 'name': 'directory', 'type': 'directory', - 'description': 'Where should Transmission saved the downloaded files?', + 'description': 'Where should Transmission save the downloaded files?', }, { 'name': 'ratio', 'default': 10, - 'type': 'int', + 'type': 'float', 'advanced': True, 'description': 'Stop transfer when reaching ratio', }, + { + 'name': 'ratiomode', + 'default': 0, + 'type': 'int', + 'advanced': True, + 'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.', + }, { 'name': 'manual', 'default': 0, diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 6c4607fb..24f7649d 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -1,12 +1,15 @@ from base64 import b64encode -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import isInt from couchpotato.core.logger import CPLog +from couchpotato.environment import Env +from datetime import timedelta import httplib import json import os.path import re import urllib2 +import shutil log = CPLog(__name__) @@ -18,7 +21,7 @@ class Transmission(Downloader): def download(self, data, movie, filedata = None): - log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) + log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) # Load host from config and split out port. host = self.conf('host').split(':') @@ -30,7 +33,7 @@ class Transmission(Downloader): folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) - # Create the empty folder to download too + # Create the empty folder to download to self.makeDir(folder_path) params = { @@ -42,7 +45,7 @@ class Transmission(Downloader): if self.conf('ratio'): torrent_params = { 'seedRatioLimit': self.conf('ratio'), - 'seedRatioMode': self.conf('ratio') + 'seedRatioMode': self.conf('ratiomode') } if not filedata and data.get('type') == 'torrent': @@ -62,11 +65,99 @@ class Transmission(Downloader): if torrent_params: trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params) + log.info('Torrent sent to Transmission successfully.') return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) except Exception, err: log.error('Failed to change settings for transfer: %s', err) return False + def getAllDownloadStatus(self): + + log.debug('Checking Transmission download status.') + + # Load host from config and split out port. + host = self.conf('host').split(':') + if not isInt(host[1]): + log.error('Config properties are not filled in correctly, port is missing.') + return False + + # Go through Queue + try: + trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) + return_params = { + 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio'] + } + queue = trpc.get_alltorrents(return_params) + + except Exception, err: + log.error('Failed getting queue: %s', err) + return False + + statuses = StatusList(self) + + # Get torrents status + # CouchPotato Status + #status = 'busy' + #status = 'failed' + #status = 'completed' + # Transmission Status + #status = 0 => "Torrent is stopped" + #status = 1 => "Queued to check files" + #status = 2 => "Checking files" + #status = 3 => "Queued to download" + #status = 4 => "Downloading" + #status = 4 => "Queued to seed" + #status = 6 => "Seeding" + #To do : + # add checking file + # manage no peer in a range time => fail + + for item in queue['torrents']: + log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished'] )) + + if not os.path.isdir(Env.setting('from', 'renamer')): + log.debug('Renamer folder has to exist.') + return + + if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'): + try: + trpc.stop_torrent(item['hashString'], {}) + + if not os.path.isdir(item['downloadDir']): + raise Exception('Missing folder: %s' % item['downloadDir']) + + else: + log.info('Moving folder from "%s" to "%s"', (item['downloadDir'], Env.setting('from', 'renamer'))) + shutil.move(item['downloadDir'], Env.setting('from', 'renamer')) + + statuses.append({ + 'id': item['hashString'], + 'name': item['downloadDir'], + 'status': 'completed', + 'original_status': item['status'], + 'timeleft': str(timedelta(seconds = 0)), + 'folder': os.path.join(Env.setting('from', 'renamer'), os.path.basename(item['downloadDir'].rstrip(os.path.sep))), + }) + trpc.remove_torrent(item['hashString'], True, {}) + except Exception, err: + log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err)) + statuses.append({ + 'id': item['hashString'], + 'name': item['downloadDir'], + 'status': 'failed', + 'original_status': item['status'], + 'timeleft': str(timedelta(seconds = 0)), + }) + else: + statuses.append({ + 'id': item['hashString'], + 'name': item['downloadDir'], + 'status': 'busy', + 'original_status': item['status'], + 'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds?? + }) + + return statuses class TransmissionRPC(object): @@ -97,6 +188,7 @@ class TransmissionRPC(object): try: open_request = urllib2.urlopen(request) response = json.loads(open_request.read()) + log.debug('request: %s', json.dumps(ojson)) log.debug('response: %s', json.dumps(response)) if response['result'] == 'success': log.debug('Transmission action successfull') @@ -146,3 +238,18 @@ class TransmissionRPC(object): arguments['ids'] = torrent_id post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag} return self._request(post_data) + + def get_alltorrents(self, arguments): + post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag} + return self._request(post_data) + + def stop_torrent(self, torrent_id, arguments): + arguments['ids'] = torrent_id + post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag} + return self._request(post_data) + + def remove_torrent(self, torrent_id, remove_local_data, arguments): + arguments['ids'] = torrent_id + arguments['delete-local-data'] = remove_local_data + post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag} + return self._request(post_data) diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index c64db135..ca0a0dae 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -5,6 +5,7 @@ from couchpotato.core.helpers.encoding import isInt, ss from couchpotato.core.logger import CPLog from hashlib import sha1 from multipartpost import MultipartPostHandler +from datetime import timedelta import cookielib import httplib import json @@ -118,7 +119,8 @@ class uTorrent(Downloader): 'name': item[2], 'status': status, 'original_status': item[1], - 'timeleft': item[10], + 'timeleft': str(timedelta(seconds = item[10])), + 'folder': '', #no fucntion to get folder, but can be deduced with getSettings function. }) return statuses diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index aba20724..ad961571 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -2,7 +2,7 @@ from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode, ss -from couchpotato.core.helpers.request import jsonified +from couchpotato.core.helpers.request import getParams, jsonified, getParam from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ getImdb from couchpotato.core.logger import CPLog @@ -31,6 +31,17 @@ class Renamer(Plugin): }) addEvent('renamer.scan', self.scan) + + addApiView('renamer.scanfolder', self.scanfolderView, docs = { + 'desc': 'For the renamer to check for new files to rename in a specified folder', + 'params': { + 'movie_folder': {'desc': 'The folder of the movie to scan'}, + 'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'}, + 'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'}, + }, + }) + + addEvent('renamer.scanfolder', self.scanfolder) addEvent('renamer.check_snatched', self.checkSnatched) addEvent('app.load', self.scan) @@ -51,6 +62,26 @@ class Renamer(Plugin): }) def scan(self): + self.scanfolder() + + def scanfolderView(self): + + params = getParams() + movie_folder = params.get('movie_folder', None) + downloader = params.get('downloader', None) + download_id = params.get('download_id', None) + + fireEventAsync('renamer.scanfolder', + movie_folder = movie_folder, + downloader = downloader, + download_id = download_id + ) + + return jsonified({ + 'success': True + }) + + def scanfolder(self, movie_folder = None, downloader = None, download_id = None): if self.isDisabled(): return @@ -59,18 +90,43 @@ class Renamer(Plugin): log.info('Renamer is already running, if you see this often, check the logs above for errors.') return + self.renaming_started = True + # Check to see if the "to" folder is inside the "from" folder. - if not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): + if movie_folder and not os.path.isdir(movie_folder): # or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): log.debug('"To" and "From" have to exist.') return elif self.conf('from') in self.conf('to'): log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.') return + elif (movie_folder and movie_folder in [self.conf('to'), self.conf('from')]): + log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.') + return - groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True) + # make sure the movie folder name is included in the search + folder = None + movie_files = [] + if movie_folder: + log.info('Scanning movie folder %s...', movie_folder) + movie_folder = movie_folder.rstrip(os.path.sep) + folder = os.path.dirname(movie_folder) - self.renaming_started = True + # Get all files from the specified folder + try: + for root, folders, names in os.walk(movie_folder): + movie_files.extend([os.path.join(root, name) for name in names]) + except: + log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc())) + groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, downloader = downloader, download_id = download_id, single = True) + + # Make sure only one movie was found if a download ID is provided + if downloader and download_id and not len(groups) == 1: + log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_id, len(groups))) + downloader = None + download_id = None + groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, single = True) + destination = self.conf('to') folder_name = self.conf('folder_name') file_name = self.conf('file_name') @@ -597,7 +653,10 @@ class Renamer(Plugin): db.commit() elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) - scan_required = True + if item['id'] and item['downloader'] and item['folder']: + fireEventAsync('renamer.scanfolder', movie_folder = item['folder'], downloader = item['downloader'], download_id = item['id']) + else: + scan_required = True found = True break diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index b2ac938c..6e9a9b6f 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString, ss from couchpotato.core.helpers.variable import getExt, getImdb, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import File, Movie +from couchpotato.core.settings.model import File, Movie, Release, ReleaseInfo from enzyme.exceptions import NoParserError, ParseError from guessit import guess_movie_info from subliminal.videos import Video @@ -101,7 +101,7 @@ class Scanner(Plugin): addEvent('scanner.name_year', self.getReleaseNameYear) addEvent('scanner.partnumber', self.getPartNumber) - def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None): + def scan(self, folder = None, files = None, downloader = None, download_id = None, simple = False, newer_than = 0, on_found = None): folder = ss(os.path.normpath(folder)) @@ -119,8 +119,7 @@ class Scanner(Plugin): try: files = [] for root, dirs, walk_files in os.walk(folder): - for filename in walk_files: - files.append(os.path.join(root, filename)) + files.extend(os.path.join(root, filename) for filename in walk_files) except: log.error('Failed getting files from %s: %s', (folder, traceback.format_exc())) else: @@ -129,6 +128,24 @@ class Scanner(Plugin): db = get_session() + # Get the release with the downloader ID that was downloded by the downloader + download_quality = None + download_imdb_id = None + if downloader and download_id: + # NOTE TO RUUD: Don't really know how to do this better... but there must be a way...? + rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader) + rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id) + for rlsnfo_dwnld in rlsnfo_dwnlds: + for rlsnfo_id in rlsnfo_ids: + if rlsnfo_id.release == rlsnfo_dwnld.release: + rls = rlsnfo_id.release + + if rls: + download_imdb_id = rls.movie.library.identifier + download_quality = rls.quality.identifier + else: + log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) + for file_path in files: if not os.path.exists(file_path): @@ -346,7 +363,7 @@ class Scanner(Plugin): continue log.debug('Getting metadata for %s', identifier) - group['meta_data'] = self.getMetaData(group, folder = folder) + group['meta_data'] = self.getMetaData(group, folder = folder, download_quality = download_quality) # Subtitle meta group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {} @@ -376,7 +393,7 @@ class Scanner(Plugin): del group['unsorted_files'] # Determine movie - group['library'] = self.determineMovie(group) + group['library'] = self.determineMovie(group, download_imdb_id = download_imdb_id) if not group['library']: log.error('Unable to determine movie: %s', group['identifiers']) else: @@ -401,7 +418,7 @@ class Scanner(Plugin): return processed_movies - def getMetaData(self, group, folder = ''): + def getMetaData(self, group, folder = '', download_quality = None): data = {} files = list(group['files']['movie']) @@ -423,10 +440,14 @@ class Scanner(Plugin): if data.get('audio'): break + # Use the quality guess first, if that failes use the quality we wanted to download data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True) if not data['quality']: - data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) - + if download_quality: + data['quality'] = fireEvent('quality.single', download_quality, single = True) + else: + data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) + data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD' filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0]) @@ -501,17 +522,19 @@ class Scanner(Plugin): return detected_languages - def determineMovie(self, group): - imdb_id = None + def determineMovie(self, group, download_imdb_id = None): + # Get imdb id from downloader + imdb_id = download_imdb_id files = group['files'] # Check for CP(imdb_id) string in the file paths - for cur_file in files['movie']: - imdb_id = self.getCPImdb(cur_file) - if imdb_id: - log.debug('Found movie via CP tag: %s', cur_file) - break + if not imdb_id: + for cur_file in files['movie']: + imdb_id = self.getCPImdb(cur_file) + if imdb_id: + log.debug('Found movie via CP tag: %s', cur_file) + break # Check and see if nfo contains the imdb-id if not imdb_id: From 0c44c486280c0024dbeeb433eb835a89ebd1d269 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 31 Mar 2013 11:12:37 +0200 Subject: [PATCH 040/127] Notification test failed. closes #1561 Thanks @FredrikWendt --- couchpotato/core/notifications/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index dda3dade..9e80aea8 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -44,7 +44,7 @@ class Notification(Provider): def _notify(self, *args, **kwargs): if self.isEnabled(): - self.notify(*args, **kwargs) + return self.notify(*args, **kwargs) def notify(self, message = '', data = {}, listener = None): pass From 8fe60a893c70eef48ab2fd565f558e042d319c2f Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sun, 31 Mar 2013 17:17:36 +0200 Subject: [PATCH 041/127] Update of uTorrent --- couchpotato/core/downloaders/utorrent/main.py | 41 ++++++++++++++++++- couchpotato/core/plugins/renamer/main.py | 2 +- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index ca0a0dae..bca57e71 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -6,6 +6,7 @@ from couchpotato.core.logger import CPLog from hashlib import sha1 from multipartpost import MultipartPostHandler from datetime import timedelta +import os import cookielib import httplib import json @@ -105,6 +106,35 @@ class uTorrent(Downloader): return False statuses = StatusList(self) + download_folder = '' + settings_dict = {} + + try: + data = self.utorrent_api.get_settings() + utorrent_settings = json.loads(data) + + # Create settings dict + for item in utorrent_settings['settings']: + if item[1] == 0: # int + settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0') + elif item[1] == 1: # bool + settings_dict[item[0]] = True if item[2] == 'true' else False + elif item[1] == 2: # string + settings_dict[item[0]] = item[2] + log.debug('uTorrent settings: %s', settings_dict) + + # Get the download path from the uTorrent settings + if settings_dict['dir_completed_download_flag']: + download_folder = settings_dict['dir_completed_download'] + elif settings_dict['dir_active_download_flag']: + download_folder = settings_dict['dir_active_download'] + else: + log.info('No download folder set in uTorrent. Please set a download folder') + return False + + except Exception, err: + log.error('Failed to get settings from uTorrent: %s', err) + return False # Get torrents for item in queue.get('torrents', []): @@ -114,13 +144,18 @@ class uTorrent(Downloader): if item[21] == 'Finished' or item[21] == 'Seeding': status = 'completed' + if settings_dict['dir_add_label']: + release_folder = os.path.join(download_folder, item[11], item[2]) + else: + release_folder = os.path.join(download_folder, item[2]) + statuses.append({ 'id': item[0], 'name': item[2], 'status': status, 'original_status': item[1], 'timeleft': str(timedelta(seconds = item[10])), - 'folder': '', #no fucntion to get folder, but can be deduced with getSettings function. + 'folder': release_folder, }) return statuses @@ -197,3 +232,7 @@ class uTorrentAPI(object): def get_status(self): action = "list=1" return self._request(action) + + def get_settings(self): + action = "action=getsettings" + return self._request(action) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index ad961571..4bd96d79 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -93,7 +93,7 @@ class Renamer(Plugin): self.renaming_started = True # Check to see if the "to" folder is inside the "from" folder. - if movie_folder and not os.path.isdir(movie_folder): # or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): + if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): log.debug('"To" and "From" have to exist.') return elif self.conf('from') in self.conf('to'): From 4cdb99a3839c511afebc40dfb52b81071660d4ca Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 31 Mar 2013 22:42:27 +0200 Subject: [PATCH 042/127] Add dvdscreen to screener quality. fix #1555 --- couchpotato/core/plugins/quality/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index c32ad81d..cf0b046c 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -24,7 +24,7 @@ class QualityPlugin(Plugin): {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, - {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, + {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, From a83c276aa2861636ce57e323d40e6de5532ec694 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 31 Mar 2013 23:50:47 +0200 Subject: [PATCH 043/127] Schedule start normal --- couchpotato/core/_base/scheduler/main.py | 52 +++++------------------- 1 file changed, 11 insertions(+), 41 deletions(-) diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py index 4102552e..2c97e1b4 100644 --- a/couchpotato/core/_base/scheduler/main.py +++ b/couchpotato/core/_base/scheduler/main.py @@ -16,51 +16,19 @@ class Scheduler(Plugin): addEvent('schedule.cron', self.cron) addEvent('schedule.interval', self.interval) - addEvent('schedule.start', self.start) - addEvent('schedule.restart', self.start) - - addEvent('app.load', self.start) + addEvent('schedule.remove', self.remove) self.sched = Sched(misfire_grace_time = 60) - - def remove(self, identifier): - for type in ['interval', 'cron']: - try: - self.sched.unschedule_job(getattr(self, type)[identifier]['job']) - log.debug('%s unscheduled %s', (type.capitalize(), identifier)) - except: - pass - - def start(self): - - # Stop all running - self.stop() - - # Crons - for identifier in self.crons: - try: - self.remove(identifier) - cron = self.crons[identifier] - job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute']) - cron['job'] = job - except ValueError, e: - log.error('Failed adding cronjob: %s', e) - - # Intervals - for identifier in self.intervals: - try: - self.remove(identifier) - interval = self.intervals[identifier] - job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds']) - interval['job'] = job - except ValueError, e: - log.error('Failed adding interval cronjob: %s', e) - - # Start it - log.debug('Starting scheduler') self.sched.start() self.started = True - log.debug('Scheduler started') + + def remove(self, identifier): + for cron_type in ['intervals', 'crons']: + try: + self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job']) + log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier)) + except: + pass def doShutdown(self): super(Scheduler, self).doShutdown() @@ -82,6 +50,7 @@ class Scheduler(Plugin): 'day': day, 'hour': hour, 'minute': minute, + 'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute) } def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0): @@ -93,4 +62,5 @@ class Scheduler(Plugin): 'hours': hours, 'minutes': minutes, 'seconds': seconds, + 'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds) } From 207e846ae6fe2147a32e96e02848e781b270695b Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 1 Apr 2013 00:06:15 +0200 Subject: [PATCH 044/127] Check crons after saving settings. fix #1556 & #1557 --- couchpotato/core/_base/updater/main.py | 10 +++++++++- couchpotato/core/plugins/automation/main.py | 7 ++++++- couchpotato/core/plugins/renamer/main.py | 20 ++++++++++++++++---- couchpotato/core/plugins/searcher/main.py | 7 ++++++- couchpotato/core/settings/__init__.py | 3 +++ 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 18d2c303..07031556 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -32,7 +32,6 @@ class Updater(Plugin): else: self.updater = SourceUpdater() - fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6) addEvent('app.load', self.autoUpdate) addEvent('updater.info', self.info) @@ -52,6 +51,15 @@ class Updater(Plugin): 'return': {'type': 'see updater.info'} }) + addEvent('setting.save.updater.enabled.after', self.setCrons) + + def setCrons(self): + + fireEvent('schedule.remove', 'updater.check', single = True) + if self.isEnabled(): + fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6) + self.autoUpdate() # Check after enabling + def autoUpdate(self): if self.check() and self.conf('automatic') and not self.updater.update_failed: if self.updater.doUpdate(): diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index f4ede40d..67bae1d1 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -10,11 +10,16 @@ class Automation(Plugin): def __init__(self): - fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) + addEvent('app.load', self.setCrons) if not Env.get('dev'): addEvent('app.load', self.addMovies) + addEvent('setting.save.automation.hour.after', self.setCrons) + + def setCrons(self): + fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) + def addMovies(self): movies = fireEvent('automation.get_movies', merge = True) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index aba20724..06487aae 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -35,12 +35,24 @@ class Renamer(Plugin): addEvent('app.load', self.scan) addEvent('app.load', self.checkSnatched) + addEvent('app.load', self.setCrons) - if self.conf('run_every') > 0: - fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every')) + # Enable / disable interval + addEvent('setting.save.renamer.enabled.after', self.setCrons) + addEvent('setting.save.renamer.run_every.after', self.setCrons) + addEvent('setting.save.renamer.force_every.after', self.setCrons) - if self.conf('force_every') > 0: - fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every')) + def setCrons(self): + + fireEvent('schedule.remove', 'renamer.check_snatched') + if self.isEnabled() and self.conf('run_every') > 0: + fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'), single = True) + + fireEvent('schedule.remove', 'renamer.check_snatched_forced') + if self.isEnabled() and self.conf('force_every') > 0: + fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'), single = True) + + return True def scanView(self): diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index a34b0b47..8a23ca4b 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -50,7 +50,12 @@ class Searcher(Plugin): }"""}, }) - # Schedule cronjob + addEvent('app.load', self.setCrons) + addEvent('setting.save.searcher.cron_day.after', self.setCrons) + addEvent('setting.save.searcher.cron_hour.after', self.setCrons) + addEvent('setting.save.searcher.cron_minute.after', self.setCrons) + + def setCrons(self): fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) def allMoviesView(self): diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 00f77a64..45443fcb 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -189,6 +189,9 @@ class Settings(object): self.set(section, option, (new_value if new_value else value).encode('unicode_escape')) self.save() + # After save (for re-interval etc) + fireEvent('setting.save.%s.%s.after' % (section, option), single = True) + return jsonified({ 'success': True, }) From 45b9919f67992b26b6f3398c2695864d389b9866 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Mon, 1 Apr 2013 10:13:24 +0200 Subject: [PATCH 045/127] Added some debugging info --- couchpotato/core/plugins/scanner/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 6e9a9b6f..c35d0418 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -525,6 +525,8 @@ class Scanner(Plugin): def determineMovie(self, group, download_imdb_id = None): # Get imdb id from downloader imdb_id = download_imdb_id + if imdb_id: + log.debug('Found movie via imdb id from it\'s download id: %s', download_imdb_id) files = group['files'] From 2851781a72e7e0708e5b48f3f6f5c15f654469b3 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Mon, 1 Apr 2013 12:56:08 +0200 Subject: [PATCH 046/127] Move around items between scanner and renamer --- couchpotato/core/plugins/renamer/main.py | 33 ++++++++++++++++-------- couchpotato/core/plugins/scanner/main.py | 28 ++++++-------------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 4bd96d79..96d84b83 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -2,12 +2,12 @@ from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode, ss -from couchpotato.core.helpers.request import getParams, jsonified, getParam +from couchpotato.core.helpers.request import getParams, jsonified from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ getImdb from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library, File, Profile, Release +from couchpotato.core.settings.model import Library, File, Profile, Release, ReleaseInfo from couchpotato.environment import Env import errno import os @@ -118,15 +118,28 @@ class Renamer(Plugin): except: log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc())) - groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, downloader = downloader, download_id = download_id, single = True) + db = get_session() - # Make sure only one movie was found if a download ID is provided - if downloader and download_id and not len(groups) == 1: - log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_id, len(groups))) - downloader = None - download_id = None - groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, single = True) + # Get the release with the downloader ID that was downloded by the downloader + download_quality = None + download_imdb_id = None + if downloader and download_id: + # NOTE TO RUUD: Don't really know how to do this better... but there must be a way...? + rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader) + rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id) + for rlsnfo_dwnld in rlsnfo_dwnlds: + for rlsnfo_id in rlsnfo_ids: + if rlsnfo_id.release == rlsnfo_dwnld.release: + rls = rlsnfo_id.release + + if rls: + download_imdb_id = rls.movie.library.identifier + download_quality = rls.quality.identifier + else: + log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) + groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, download_quality = download_quality, download_imdb_id = download_imdb_id, single = True) + destination = self.conf('to') folder_name = self.conf('folder_name') file_name = self.conf('file_name') @@ -140,8 +153,6 @@ class Renamer(Plugin): downloaded_status = fireEvent('status.get', 'downloaded', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True) - db = get_session() - for group_identifier in groups: group = groups[group_identifier] diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index c35d0418..d714481e 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString, ss from couchpotato.core.helpers.variable import getExt, getImdb, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import File, Movie, Release, ReleaseInfo +from couchpotato.core.settings.model import File, Movie from enzyme.exceptions import NoParserError, ParseError from guessit import guess_movie_info from subliminal.videos import Video @@ -101,7 +101,7 @@ class Scanner(Plugin): addEvent('scanner.name_year', self.getReleaseNameYear) addEvent('scanner.partnumber', self.getPartNumber) - def scan(self, folder = None, files = None, downloader = None, download_id = None, simple = False, newer_than = 0, on_found = None): + def scan(self, folder = None, files = None, download_imdb_id = None, download_quality = None, simple = False, newer_than = 0, on_found = None): folder = ss(os.path.normpath(folder)) @@ -128,24 +128,6 @@ class Scanner(Plugin): db = get_session() - # Get the release with the downloader ID that was downloded by the downloader - download_quality = None - download_imdb_id = None - if downloader and download_id: - # NOTE TO RUUD: Don't really know how to do this better... but there must be a way...? - rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader) - rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id) - for rlsnfo_dwnld in rlsnfo_dwnlds: - for rlsnfo_id in rlsnfo_ids: - if rlsnfo_id.release == rlsnfo_dwnld.release: - rls = rlsnfo_id.release - - if rls: - download_imdb_id = rls.movie.library.identifier - download_quality = rls.quality.identifier - else: - log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) - for file_path in files: if not os.path.exists(file_path): @@ -330,6 +312,12 @@ class Scanner(Plugin): valid_files[identifier] = group del movie_files + + # Make sure only one movie was found if a download ID is provided + if download_imdb_id and download_quality and not len(valid_files) == 1: + log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_imdb_id, len(valid_files))) + download_imdb_id = None + download_quality = None # Determine file types processed_movies = {} From 33a6a7d3a08b6d3502b89d838b06ab370aad0291 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 1 Apr 2013 17:31:04 +0200 Subject: [PATCH 047/127] CP Provider API Identifier --- couchpotato/core/providers/movie/couchpotatoapi/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/providers/movie/couchpotatoapi/main.py b/couchpotato/core/providers/movie/couchpotatoapi/main.py index fc16e8ab..8415c2d0 100644 --- a/couchpotato/core/providers/movie/couchpotatoapi/main.py +++ b/couchpotato/core/providers/movie/couchpotatoapi/main.py @@ -5,6 +5,7 @@ from couchpotato.core.helpers.request import jsonified, getParams from couchpotato.core.logger import CPLog from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.settings.model import Movie +from couchpotato.environment import Env import time log = CPLog(__name__) @@ -96,4 +97,5 @@ class CouchPotatoApi(MovieProvider): 'X-CP-Version': fireEvent('app.version', single = True), 'X-CP-API': self.api_version, 'X-CP-Time': time.time(), + 'X-CP-Identifier': '+%s' % Env.setting('api_key', 'core')[:10], # Use first 10 as identifier, so we don't need to use IP address in api stats } From c7ee8a063560529fd7c52d5bf5c98cd77fe23c28 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 1 Apr 2013 20:11:49 +0200 Subject: [PATCH 048/127] Don't use object in correctmovie event --- couchpotato/core/plugins/searcher/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 8a23ca4b..ad33c6ff 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -371,7 +371,7 @@ class Searcher(Plugin): return search_types - def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): + def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs): imdb_results = kwargs.get('imdb_results', False) retention = Env.setting('retention', section = 'nzb') From 3fe7d2ea154bba95c9e26384df877692fa5c0ff2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 1 Apr 2013 21:18:29 +0200 Subject: [PATCH 049/127] Download id cleanup --- couchpotato/core/plugins/renamer/main.py | 65 ++++++++---------------- couchpotato/core/plugins/scanner/main.py | 30 +++++------ 2 files changed, 37 insertions(+), 58 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index de35943e..e3291f64 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -7,7 +7,8 @@ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ getImdb from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library, File, Profile, Release, ReleaseInfo +from couchpotato.core.settings.model import Library, File, Profile, Release, \ + ReleaseInfo from couchpotato.environment import Env import errno import os @@ -27,21 +28,15 @@ class Renamer(Plugin): def __init__(self): addApiView('renamer.scan', self.scanView, docs = { - 'desc': 'For the renamer to check for new files to rename', - }) - - addEvent('renamer.scan', self.scan) - - addApiView('renamer.scanfolder', self.scanfolderView, docs = { - 'desc': 'For the renamer to check for new files to rename in a specified folder', + 'desc': 'For the renamer to check for new files to rename in a folder', 'params': { - 'movie_folder': {'desc': 'The folder of the movie to scan'}, + 'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'}, 'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'}, 'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'}, }, }) - addEvent('renamer.scanfolder', self.scanfolder) + addEvent('renamer.scan', self.scan) addEvent('renamer.check_snatched', self.checkSnatched) addEvent('app.load', self.scan) @@ -67,33 +62,22 @@ class Renamer(Plugin): def scanView(self): - fireEventAsync('renamer.scan') - - return jsonified({ - 'success': True - }) - - def scan(self): - self.scanfolder() - - def scanfolderView(self): - params = getParams() movie_folder = params.get('movie_folder', None) downloader = params.get('downloader', None) download_id = params.get('download_id', None) - fireEventAsync('renamer.scanfolder', - movie_folder = movie_folder, - downloader = downloader, + fireEventAsync('renamer.scan', + movie_folder = movie_folder, + downloader = downloader, download_id = download_id ) return jsonified({ 'success': True }) - - def scanfolder(self, movie_folder = None, downloader = None, download_id = None): + + def scan(self, movie_folder = None, downloader = None, download_id = None): if self.isDisabled(): return @@ -116,7 +100,7 @@ class Renamer(Plugin): return # make sure the movie folder name is included in the search - folder = None + folder = None movie_files = [] if movie_folder: log.info('Scanning movie folder %s...', movie_folder) @@ -133,24 +117,19 @@ class Renamer(Plugin): db = get_session() # Get the release with the downloader ID that was downloded by the downloader - download_quality = None - download_imdb_id = None - if downloader and download_id: - # NOTE TO RUUD: Don't really know how to do this better... but there must be a way...? - rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader) - rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id) - for rlsnfo_dwnld in rlsnfo_dwnlds: - for rlsnfo_id in rlsnfo_ids: - if rlsnfo_id.release == rlsnfo_dwnld.release: - rls = rlsnfo_id.release + download_info = None + if download_id: + rls_info = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).first() - if rls: - download_imdb_id = rls.movie.library.identifier - download_quality = rls.quality.identifier + if rls_info: + download_info = { + 'imdb_id': rls_info.release.movie.library.identifier, + 'quality': rls_info.release.quality.identifier, + } else: log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) - - groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, download_quality = download_quality, download_imdb_id = download_imdb_id, single = True) + + groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, download_info = download_info, single = True) destination = self.conf('to') folder_name = self.conf('folder_name') @@ -677,7 +656,7 @@ class Renamer(Plugin): elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) if item['id'] and item['downloader'] and item['folder']: - fireEventAsync('renamer.scanfolder', movie_folder = item['folder'], downloader = item['downloader'], download_id = item['id']) + fireEventAsync('renamer.scan', movie_folder = item['folder'], downloader = item['downloader'], download_id = item['id']) else: scan_required = True diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index d714481e..e7a94598 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -101,7 +101,7 @@ class Scanner(Plugin): addEvent('scanner.name_year', self.getReleaseNameYear) addEvent('scanner.partnumber', self.getPartNumber) - def scan(self, folder = None, files = None, download_imdb_id = None, download_quality = None, simple = False, newer_than = 0, on_found = None): + def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, on_found = None): folder = ss(os.path.normpath(folder)) @@ -312,12 +312,11 @@ class Scanner(Plugin): valid_files[identifier] = group del movie_files - + # Make sure only one movie was found if a download ID is provided - if download_imdb_id and download_quality and not len(valid_files) == 1: - log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_imdb_id, len(valid_files))) - download_imdb_id = None - download_quality = None + if download_info and not len(valid_files) == 1: + log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files))) + download_info = None # Determine file types processed_movies = {} @@ -351,7 +350,7 @@ class Scanner(Plugin): continue log.debug('Getting metadata for %s', identifier) - group['meta_data'] = self.getMetaData(group, folder = folder, download_quality = download_quality) + group['meta_data'] = self.getMetaData(group, folder = folder, download_info = download_info) # Subtitle meta group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {} @@ -381,7 +380,7 @@ class Scanner(Plugin): del group['unsorted_files'] # Determine movie - group['library'] = self.determineMovie(group, download_imdb_id = download_imdb_id) + group['library'] = self.determineMovie(group, download_info = download_info) if not group['library']: log.error('Unable to determine movie: %s', group['identifiers']) else: @@ -406,7 +405,7 @@ class Scanner(Plugin): return processed_movies - def getMetaData(self, group, folder = '', download_quality = None): + def getMetaData(self, group, folder = '', download_info = None): data = {} files = list(group['files']['movie']) @@ -431,11 +430,11 @@ class Scanner(Plugin): # Use the quality guess first, if that failes use the quality we wanted to download data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True) if not data['quality']: - if download_quality: - data['quality'] = fireEvent('quality.single', download_quality, single = True) + if download_info and download_info.get('quality'): + data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True) else: data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) - + data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD' filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0]) @@ -510,11 +509,12 @@ class Scanner(Plugin): return detected_languages - def determineMovie(self, group, download_imdb_id = None): + def determineMovie(self, group, download_info = None): + # Get imdb id from downloader - imdb_id = download_imdb_id + imdb_id = download_info and download_info.get('imdb_id') if imdb_id: - log.debug('Found movie via imdb id from it\'s download id: %s', download_imdb_id) + log.debug('Found movie via imdb id from it\'s download id: %s', download_info.get('imdb_id')) files = group['files'] From 72cc3576d39decf370fa7408ab1cf5bce46fcf10 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 1 Apr 2013 22:51:34 +0200 Subject: [PATCH 050/127] Use @mano3m code to check download_info --- couchpotato/core/plugins/renamer/main.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index e3291f64..a6260d7b 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -118,13 +118,23 @@ class Renamer(Plugin): # Get the release with the downloader ID that was downloded by the downloader download_info = None - if download_id: - rls_info = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).first() + if download_id and downloader: + rls = None - if rls_info: + rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader).all() + rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).all() + + for rlsnfo_dwnld in rlsnfo_dwnlds: + for rlsnfo_id in rlsnfo_ids: + if rlsnfo_id.release == rlsnfo_dwnld.release: + rls = rlsnfo_id.release + break + if rls: break + + if rls: download_info = { - 'imdb_id': rls_info.release.movie.library.identifier, - 'quality': rls_info.release.quality.identifier, + 'imdb_id': rls.movie.library.identifier, + 'quality': rls.quality.identifier, } else: log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) @@ -582,6 +592,7 @@ class Renamer(Plugin): loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) def checkSnatched(self): + if self.checking_snatched: log.debug('Already checking snatched') From 63609bb52cb4a670a45a168eccfa8ad247734c2f Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Fri, 5 Apr 2013 00:05:09 +0200 Subject: [PATCH 051/127] Fix Transmission --- .../core/downloaders/transmission/main.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 31c2f61e..0249bdac 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -128,27 +128,28 @@ class Transmission(Downloader): try: trpc.stop_torrent(item['hashString'], {}) - if not os.path.isdir(item['downloadDir']): - raise Exception('Missing folder: %s' % item['downloadDir']) - + if not os.path.isdir(os.path.join(item['downloadDir'], item['name'])): + raise Exception('Missing folder: %s' % os.path.join(item['downloadDir'], item['name'])) + elif item['downloadDir'].rstrip(os.path.sep) in Env.setting('from', 'renamer').rstrip(os.path.sep): + raise Exception('Transmission download folder should not be the same or in the renamer "from" folder!') else: - log.info('Moving folder from "%s" to "%s"', (item['downloadDir'], Env.setting('from', 'renamer'))) - shutil.move(item['downloadDir'], Env.setting('from', 'renamer')) - + log.info('Moving folder from "%s" to "%s"', (os.path.join(item['downloadDir'], item['name']), Env.setting('from', 'renamer'))) + shutil.move(os.path.join(item['downloadDir'], item['name']), Env.setting('from', 'renamer')) + statuses.append({ 'id': item['hashString'], - 'name': item['downloadDir'], + 'name': item['name'], 'status': 'completed', 'original_status': item['status'], 'timeleft': str(timedelta(seconds = 0)), - 'folder': os.path.join(Env.setting('from', 'renamer'), os.path.basename(item['downloadDir'].rstrip(os.path.sep))), + 'folder': os.path.join(Env.setting('from', 'renamer'), item['name']), }) trpc.remove_torrent(item['hashString'], True, {}) except Exception, err: log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err)) statuses.append({ 'id': item['hashString'], - 'name': item['downloadDir'], + 'name': item['name'], 'status': 'failed', 'original_status': item['status'], 'timeleft': str(timedelta(seconds = 0)), @@ -156,7 +157,7 @@ class Transmission(Downloader): else: statuses.append({ 'id': item['hashString'], - 'name': item['downloadDir'], + 'name': item['name'], 'status': 'busy', 'original_status': item['status'], 'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds?? From 7f4373e0002450ac3a6709449869c024b264184d Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 5 Apr 2013 12:08:45 +0200 Subject: [PATCH 052/127] Traceback import missing --- couchpotato/core/downloaders/transmission/main.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 0249bdac..6a5aad10 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -8,8 +8,9 @@ import httplib import json import os.path import re -import urllib2 import shutil +import traceback +import urllib2 log = CPLog(__name__) @@ -118,7 +119,7 @@ class Transmission(Downloader): # manage no peer in a range time => fail for item in queue['torrents']: - log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished'] )) + log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished'])) if not os.path.isdir(Env.setting('from', 'renamer')): log.debug('Renamer folder has to exist.') @@ -129,13 +130,13 @@ class Transmission(Downloader): trpc.stop_torrent(item['hashString'], {}) if not os.path.isdir(os.path.join(item['downloadDir'], item['name'])): - raise Exception('Missing folder: %s' % os.path.join(item['downloadDir'], item['name'])) + raise Exception('Missing folder: %s' % os.path.join(item['downloadDir'], item['name'])) elif item['downloadDir'].rstrip(os.path.sep) in Env.setting('from', 'renamer').rstrip(os.path.sep): raise Exception('Transmission download folder should not be the same or in the renamer "from" folder!') else: log.info('Moving folder from "%s" to "%s"', (os.path.join(item['downloadDir'], item['name']), Env.setting('from', 'renamer'))) shutil.move(os.path.join(item['downloadDir'], item['name']), Env.setting('from', 'renamer')) - + statuses.append({ 'id': item['hashString'], 'name': item['name'], From 57ae06e139ebf501b450e238395ce0ff346eaada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20K=C3=A5berg?= Date: Fri, 5 Apr 2013 13:48:09 +0200 Subject: [PATCH 053/127] initial link support --- couchpotato/core/plugins/renamer/__init__.py | 14 ++++ couchpotato/core/plugins/renamer/main.py | 8 ++- libs/linktastic/README.txt | 19 +++++ libs/linktastic/__init__.py | 0 libs/linktastic/linktastic.py | 76 ++++++++++++++++++++ libs/linktastic/setup.py | 13 ++++ 6 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 libs/linktastic/README.txt create mode 100644 libs/linktastic/__init__.py create mode 100644 libs/linktastic/linktastic.py create mode 100644 libs/linktastic/setup.py diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 22277ba5..0e46396c 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -112,6 +112,20 @@ config = [{ 'label': 'Separator', 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', }, + { + 'name': 'hardlink', + 'type': 'bool', + 'description': 'Use hard links instead of moving files, ideal for Torrent users. NOTE: From location and to location needs to be on the same drive/partition.', + 'default': False, + 'advanced': True, + }, + { + 'name': 'symlink', + 'type': 'bool', + 'description': 'Use sym links instead of moving files.', + 'default': False, + 'advanced': True, + }, { 'advanced': True, 'name': 'ntfs_permission', diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index a6260d7b..cd3447d2 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -10,6 +10,7 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Library, File, Profile, Release, \ ReleaseInfo from couchpotato.environment import Env +import linktastic.linktastic as linktastic import errno import os import re @@ -523,7 +524,12 @@ class Renamer(Plugin): def moveFile(self, old, dest): dest = ss(dest) try: - shutil.move(old, dest) + if self.conf('hardlink') and not self.conf('symlink'): + linktastic.link(old, dest) + elif self.conf('symlink') and not self.conf('hardlink'): + linktastic.symlink(old, dest) + else: + shutil.move(old, dest) try: os.chmod(dest, Env.getPermission('file')) diff --git a/libs/linktastic/README.txt b/libs/linktastic/README.txt new file mode 100644 index 00000000..7fad24db --- /dev/null +++ b/libs/linktastic/README.txt @@ -0,0 +1,19 @@ +Linktastic + +Linktastic is an extension of the os.link and os.symlink functionality provided +by the python language since version 2. Python only supports file linking on +*NIX-based systems, even though it is relatively simple to engineer a solution +to utilize NTFS's built-in linking functionality. Linktastic attempts to unify +linking on the windows platform with linking on *NIX-based systems. + +Usage + +Linktastic is a single python module and can be imported as such. Examples: + +# Hard linking src to dest +import linktastic +linktastic.link(src, dest) + +# Symlinking src to dest +import linktastic +linktastic.symlink(src, dest) diff --git a/libs/linktastic/__init__.py b/libs/linktastic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/linktastic/linktastic.py b/libs/linktastic/linktastic.py new file mode 100644 index 00000000..76687666 --- /dev/null +++ b/libs/linktastic/linktastic.py @@ -0,0 +1,76 @@ +# Linktastic Module +# - A python2/3 compatible module that can create hardlinks/symlinks on windows-based systems +# +# Linktastic is distributed under the MIT License. The follow are the terms and conditions of using Linktastic. +# +# The MIT License (MIT) +# Copyright (c) 2012 Solipsis Development +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +# associated documentation files (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or substantial +# portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import subprocess +from subprocess import CalledProcessError +import os + + +# Prevent spaces from messing with us! +def _escape_param(param): + return '"%s"' % param + + +# Private function to create link on nt-based systems +def _link_windows(src, dest): + try: + subprocess.check_output( + 'cmd /C mklink /H %s %s' % (_escape_param(dest), _escape_param(src)), + stderr=subprocess.STDOUT) + except CalledProcessError as err: + + raise IOError(err.output.decode('utf-8')) + + # TODO, find out what kind of messages Windows sends us from mklink + # print(stdout) + # assume if they ret-coded 0 we're good + + +def _symlink_windows(src, dest): + try: + subprocess.check_output( + 'cmd /C mklink %s %s' % (_escape_param(dest), _escape_param(src)), + stderr=subprocess.STDOUT) + except CalledProcessError as err: + raise IOError(err.output.decode('utf-8')) + + # TODO, find out what kind of messages Windows sends us from mklink + # print(stdout) + # assume if they ret-coded 0 we're good + + +# Create a hard link to src named as dest +# This version of link, unlike os.link, supports nt systems as well +def link(src, dest): + if os.name == 'nt': + _link_windows(src, dest) + else: + os.link(src, dest) + + +# Create a symlink to src named as dest, but don't fail if you're on nt +def symlink(src, dest): + if os.name == 'nt': + _symlink_windows(src, dest) + else: + os.symlink(src, dest) diff --git a/libs/linktastic/setup.py b/libs/linktastic/setup.py new file mode 100644 index 00000000..e15cc2b7 --- /dev/null +++ b/libs/linktastic/setup.py @@ -0,0 +1,13 @@ +from distutils.core import setup + +setup( + name='Linktastic', + version='0.1.0', + author='Jon "Berkona" Monroe', + author_email='solipsis.dev@gmail.com', + py_modules=['linktastic'], + url="http://github.com/berkona/linktastic", + license='MIT License - See http://opensource.org/licenses/MIT for details', + description='Truly platform-independent file linking', + long_description=open('README.txt').read(), +) From 47ddf31f7605724e7071d7b9317c8d170931847a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20K=C3=A5berg?= Date: Fri, 5 Apr 2013 14:07:10 +0200 Subject: [PATCH 054/127] ruud was bulling me ;) --- couchpotato/core/plugins/renamer/__init__.py | 16 +++++----------- couchpotato/core/plugins/renamer/main.py | 4 ++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 0e46396c..d80618bd 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -113,17 +113,11 @@ config = [{ 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', }, { - 'name': 'hardlink', - 'type': 'bool', - 'description': 'Use hard links instead of moving files, ideal for Torrent users. NOTE: From location and to location needs to be on the same drive/partition.', - 'default': False, - 'advanced': True, - }, - { - 'name': 'symlink', - 'type': 'bool', - 'description': 'Use sym links instead of moving files.', - 'default': False, + 'name': 'links', + 'default': '0', + 'type': 'dropdown', + 'values': [('Disable', 0), ('Hard link', 'hard'), ('Sym link', 'sym')], + 'description': 'Use hard links or sym links instead of moving files, ideal for Torrent users.', 'advanced': True, }, { diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index cd3447d2..e4becf52 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -524,9 +524,9 @@ class Renamer(Plugin): def moveFile(self, old, dest): dest = ss(dest) try: - if self.conf('hardlink') and not self.conf('symlink'): + if self.conf('link') == 'hard': linktastic.link(old, dest) - elif self.conf('symlink') and not self.conf('hardlink'): + elif self.conf('link') == 'sym': linktastic.symlink(old, dest) else: shutil.move(old, dest) From 3c2a00b17b589c15005029a678a0b2e79ee0e846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20K=C3=A5berg?= Date: Fri, 5 Apr 2013 14:21:15 +0200 Subject: [PATCH 055/127] add copy and move to file actions --- couchpotato/core/plugins/renamer/__init__.py | 9 +++++---- couchpotato/core/plugins/renamer/main.py | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index d80618bd..f4ea8a75 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -113,11 +113,12 @@ config = [{ 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', }, { - 'name': 'links', - 'default': '0', + 'name': 'file_action', + 'label': 'File Action', + 'default': 'move', 'type': 'dropdown', - 'values': [('Disable', 0), ('Hard link', 'hard'), ('Sym link', 'sym')], - 'description': 'Use hard links or sym links instead of moving files, ideal for Torrent users.', + 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink')], + 'description': 'Define which kind of file operation you want to use. Before you start using hard links or sym links, read about thire possible drawbacks.', 'advanced': True, }, { diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index e4becf52..d5509b0d 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -524,10 +524,12 @@ class Renamer(Plugin): def moveFile(self, old, dest): dest = ss(dest) try: - if self.conf('link') == 'hard': + if self.conf('file_action') == 'hardlink': linktastic.link(old, dest) - elif self.conf('link') == 'sym': + elif self.conf('file_action') == 'symlink': linktastic.symlink(old, dest) + elif self.conf('file_action') == 'copy': + shutil.copy(old, dest) else: shutil.move(old, dest) From 5b0fa9054b78a9a01cd174805fb03ab353c57e78 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 5 Apr 2013 17:51:19 +0200 Subject: [PATCH 056/127] Put renaming started lower --- couchpotato/core/plugins/renamer/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index a6260d7b..a880abdb 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -86,11 +86,9 @@ class Renamer(Plugin): log.info('Renamer is already running, if you see this often, check the logs above for errors.') return - self.renaming_started = True - # Check to see if the "to" folder is inside the "from" folder. if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): - log.debug('"To" and "From" have to exist.') + log.error('"To" and "From" have to exist.') return elif self.conf('from') in self.conf('to'): log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.') @@ -99,6 +97,8 @@ class Renamer(Plugin): log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.') return + self.renaming_started = True + # make sure the movie folder name is included in the search folder = None movie_files = [] From ac045539d165456c4c37e2f3b8b7fcc1123483df Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 5 Apr 2013 17:51:45 +0200 Subject: [PATCH 057/127] Don't move or delete anything in status check --- couchpotato/core/downloaders/transmission/main.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 6a5aad10..8cf47380 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -122,30 +122,20 @@ class Transmission(Downloader): log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished'])) if not os.path.isdir(Env.setting('from', 'renamer')): - log.debug('Renamer folder has to exist.') + log.error('Renamer "from" folder doesn\'t to exist.') return if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'): try: trpc.stop_torrent(item['hashString'], {}) - - if not os.path.isdir(os.path.join(item['downloadDir'], item['name'])): - raise Exception('Missing folder: %s' % os.path.join(item['downloadDir'], item['name'])) - elif item['downloadDir'].rstrip(os.path.sep) in Env.setting('from', 'renamer').rstrip(os.path.sep): - raise Exception('Transmission download folder should not be the same or in the renamer "from" folder!') - else: - log.info('Moving folder from "%s" to "%s"', (os.path.join(item['downloadDir'], item['name']), Env.setting('from', 'renamer'))) - shutil.move(os.path.join(item['downloadDir'], item['name']), Env.setting('from', 'renamer')) - statuses.append({ 'id': item['hashString'], 'name': item['name'], 'status': 'completed', 'original_status': item['status'], 'timeleft': str(timedelta(seconds = 0)), - 'folder': os.path.join(Env.setting('from', 'renamer'), item['name']), + 'folder': os.path.join(item['downloadDir'], item['name']), }) - trpc.remove_torrent(item['hashString'], True, {}) except Exception, err: log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err)) statuses.append({ From a600430be44a5b798b212353e913a478379b0c21 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 5 Apr 2013 21:56:33 +0200 Subject: [PATCH 058/127] file_action cleanup tag ignored/failed/renamed with custom file --- couchpotato/core/plugins/renamer/__init__.py | 2 +- couchpotato/core/plugins/renamer/main.py | 40 +++++++++----------- couchpotato/core/plugins/scanner/main.py | 18 +++++++-- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index f4ea8a75..f90828bc 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -118,7 +118,7 @@ config = [{ 'default': 'move', 'type': 'dropdown', 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink')], - 'description': 'Define which kind of file operation you want to use. Before you start using hard links or sym links, read about thire possible drawbacks.', + 'description': 'Define which kind of file operation you want to use. Before you start using hard links or sym links, PLEASE read about their possible drawbacks.', 'advanced': True, }, { diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 0dbda0da..26aa9dea 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -10,8 +10,8 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Library, File, Profile, Release, \ ReleaseInfo from couchpotato.environment import Env -import linktastic.linktastic as linktastic import errno +import linktastic.linktastic as linktastic import os import re import shutil @@ -140,7 +140,8 @@ class Renamer(Plugin): else: log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) - groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, download_info = download_info, single = True) + groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), + files = movie_files, download_info = download_info, return_ignored = False, single = True) destination = self.conf('to') folder_name = self.conf('folder_name') @@ -450,6 +451,9 @@ class Renamer(Plugin): log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) self.tagDir(group, 'failed_rename') + if self.conf('file_action') != 'move': + self.tagDir(group, 'renamed already') + # Remove matching releases for release in remove_releases: log.debug('Removing release %s', release.identifier) @@ -495,31 +499,23 @@ class Renamer(Plugin): return rename_files + # This adds a file to ignore / tag a release so it is ignored later def tagDir(self, group, tag): - rename_files = {} + ignore_file = None + for movie_file in sorted(list(group['files']['movie'])): + ignore_file = '%s.ignore' % os.path.splitext(movie_file)[0] + break - if group['dirname']: - rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname'])) - else: # Add it to filename - for file_type in group['files']: - for rename_me in group['files'][file_type]: - filename = os.path.basename(rename_me) - rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename)) + text = """This file is from CouchPotato +It has marked this release as "%s" +This file hides the release from the renamer +Remove it if you want it to be renamed (again, or at least let it try again) +""" % tag - for src in rename_files: - if rename_files[src]: - dst = rename_files[src] - log.info('Renaming "%s" to "%s"', (src, dst)) + if ignore_file: + self.createFile(ignore_file, text) - # Create dir - self.makeDir(os.path.dirname(dst)) - - try: - self.moveFile(src, dst) - except: - log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) - raise def moveFile(self, old, dest): dest = ss(dest) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index e7a94598..79fe0018 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -101,7 +101,7 @@ class Scanner(Plugin): addEvent('scanner.name_year', self.getReleaseNameYear) addEvent('scanner.partnumber', self.getPartNumber) - def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, on_found = None): + def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, return_ignored = True, on_found = None): folder = ss(os.path.normpath(folder)) @@ -177,17 +177,25 @@ class Scanner(Plugin): # Group files minus extension + ignored_identifiers = [] for identifier, group in movie_files.iteritems(): if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier) log.debug('Grouping files: %s', identifier) + has_ignored = 0 for file_path in group['unsorted_files']: - wo_ext = file_path[:-(len(getExt(file_path)) + 1)] + ext = getExt(file_path) + wo_ext = file_path[:-(len(ext) + 1)] found_files = set([i for i in leftovers if wo_ext in i]) group['unsorted_files'].extend(found_files) leftovers = leftovers - found_files + has_ignored += 1 if ext == 'ignore' else 0 + + if has_ignored > 0: + ignored_identifiers.append(identifier) + # Break if CP wants to shut down if self.shuttingDown(): break @@ -327,15 +335,17 @@ class Scanner(Plugin): except: break + if return_ignored is False and identifier in ignored_identifiers: + log.debug('Ignore file found, ignoring release: %s' % identifier) + continue + # Group extra (and easy) files first - # images = self.getImages(group['unsorted_files']) group['files'] = { 'movie_extra': self.getMovieExtras(group['unsorted_files']), 'subtitle': self.getSubtitles(group['unsorted_files']), 'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']), 'nfo': self.getNfo(group['unsorted_files']), 'trailer': self.getTrailers(group['unsorted_files']), - #'backdrop': images['backdrop'], 'leftover': set(group['unsorted_files']), } From 5fd4312ff807279d79756817d035099eff971eca Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 5 Apr 2013 23:50:28 +0200 Subject: [PATCH 059/127] Use simpler file.cache --- couchpotato/core/plugins/file/main.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index 0dc01783..87898f52 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.settings.model import FileType, File from couchpotato.environment import Env +from flask.helpers import send_file +from werkzeug.exceptions import NotFound import os.path import time import traceback @@ -81,11 +83,13 @@ class FileManager(Plugin): def showCacheFile(self, filename = ''): - cache_dir = Env.get('cache_dir') - filename = os.path.basename(filename) + file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename)) - from flask.helpers import send_from_directory - return send_from_directory(cache_dir, filename) + if not os.path.isfile(file_path): + log.error('File "%s" not found', file_path) + raise NotFound() + + return send_file(file_path, conditional = True) def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}): From a7bd8c822aee00408b3078afacb7bac1fe5f0b75 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 8 Apr 2013 20:55:42 +0200 Subject: [PATCH 060/127] Simplify nonblocking requests --- couchpotato/api.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/couchpotato/api.py b/couchpotato/api.py index 718527c9..58fd3102 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -11,16 +11,12 @@ api_nonblock = {} class NonBlockHandler(RequestHandler): - def __init__(self, application, request, **kwargs): - cls = NonBlockHandler - cls.stoppers = [] - super(NonBlockHandler, self).__init__(application, request, **kwargs) + stoppers = [] @asynchronous def get(self, route): - cls = NonBlockHandler start, stop = api_nonblock[route] - cls.stoppers.append(stop) + self.stoppers.append(stop) start(self.onNewMessage, last_id = self.get_argument("last_id", None)) @@ -30,12 +26,11 @@ class NonBlockHandler(RequestHandler): self.finish(response) def on_connection_close(self): - cls = NonBlockHandler - for stop in cls.stoppers: + for stop in self.stoppers: stop(self.onNewMessage) - cls.stoppers = [] + self.stoppers = [] def addApiView(route, func, static = False, docs = None, **kwargs): From c1937ea71f3ad32461739a739b53febd47862185 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 8 Apr 2013 21:10:46 +0200 Subject: [PATCH 061/127] linktastic clenaup --- couchpotato/core/plugins/renamer/main.py | 6 +++--- libs/linktastic/README.txt | 19 ------------------- libs/linktastic/setup.py | 13 ------------- 3 files changed, 3 insertions(+), 35 deletions(-) delete mode 100644 libs/linktastic/README.txt delete mode 100644 libs/linktastic/setup.py diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 26aa9dea..40e1ac6d 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -10,8 +10,8 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Library, File, Profile, Release, \ ReleaseInfo from couchpotato.environment import Env +from linktastic.linktastic import link, symlink import errno -import linktastic.linktastic as linktastic import os import re import shutil @@ -521,9 +521,9 @@ Remove it if you want it to be renamed (again, or at least let it try again) dest = ss(dest) try: if self.conf('file_action') == 'hardlink': - linktastic.link(old, dest) + link(old, dest) elif self.conf('file_action') == 'symlink': - linktastic.symlink(old, dest) + symlink(old, dest) elif self.conf('file_action') == 'copy': shutil.copy(old, dest) else: diff --git a/libs/linktastic/README.txt b/libs/linktastic/README.txt deleted file mode 100644 index 7fad24db..00000000 --- a/libs/linktastic/README.txt +++ /dev/null @@ -1,19 +0,0 @@ -Linktastic - -Linktastic is an extension of the os.link and os.symlink functionality provided -by the python language since version 2. Python only supports file linking on -*NIX-based systems, even though it is relatively simple to engineer a solution -to utilize NTFS's built-in linking functionality. Linktastic attempts to unify -linking on the windows platform with linking on *NIX-based systems. - -Usage - -Linktastic is a single python module and can be imported as such. Examples: - -# Hard linking src to dest -import linktastic -linktastic.link(src, dest) - -# Symlinking src to dest -import linktastic -linktastic.symlink(src, dest) diff --git a/libs/linktastic/setup.py b/libs/linktastic/setup.py deleted file mode 100644 index e15cc2b7..00000000 --- a/libs/linktastic/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -from distutils.core import setup - -setup( - name='Linktastic', - version='0.1.0', - author='Jon "Berkona" Monroe', - author_email='solipsis.dev@gmail.com', - py_modules=['linktastic'], - url="http://github.com/berkona/linktastic", - license='MIT License - See http://opensource.org/licenses/MIT for details', - description='Truly platform-independent file linking', - long_description=open('README.txt').read(), -) From 95a68af795dc73326b2f39579212ec1cf45a4222 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 8 Apr 2013 21:40:17 +0200 Subject: [PATCH 062/127] Debug logs --- couchpotato/core/plugins/renamer/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 40e1ac6d..4b1fab13 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -89,7 +89,8 @@ class Renamer(Plugin): # Check to see if the "to" folder is inside the "from" folder. if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): - log.error('"To" and "From" have to exist.') + l = log.debug if movie_folder else log.error + l('Both the "To" and "From" have to exist.') return elif self.conf('from') in self.conf('to'): log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.') From 66d9d853af46fbe565de874a08010a162a3700ec Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 8 Apr 2013 21:44:36 +0200 Subject: [PATCH 063/127] NZB ids not persistent in new sessions --- couchpotato/core/downloaders/nzbget/main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index d0533d5c..78e2b957 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -52,9 +52,7 @@ class NZBGet(Downloader): if xml_response: log.info('NZB sent successfully to NZBGet') - groups = rpc.listgroups() - nzb_id = [item['NZBID'] for item in groups if item['NZBFilename'] == nzb_name][0] - return self.downloadReturnId(nzb_id) + return True else: log.error('NZBGet could not add %s to the queue.', nzb_name) return False From be595aba91e27e1eeba7b751b2af325314620713 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 12 Apr 2013 20:57:00 +0200 Subject: [PATCH 064/127] Loading in movie lists --- couchpotato/core/plugins/movie/static/list.js | 36 ++++++++++++++++--- .../core/plugins/movie/static/movie.css | 29 ++++++++++++++- couchpotato/static/scripts/page/home.js | 17 +++++++-- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index 3d5545e3..a89f89af 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -6,6 +6,7 @@ var MovieList = new Class({ navigation: true, limit: 50, load_more: true, + loader: true, menu: [], add_new: false }, @@ -251,7 +252,7 @@ var MovieList = new Class({ 'data': Object.merge({ 'status': self.options.status }, self.filter), - 'onComplete': function(json){ + 'onSuccess': function(json){ json.chars.split('').each(function(c){ self.letters[c.capitalize()].addClass('available') @@ -475,12 +476,39 @@ var MovieList = new Class({ self.load_more.set('text', 'loading...'); } + if(self.movies.length == 0 && self.options.loader){ + + self.loader_first = new Element('div.loading').adopt( + new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'}) + ).inject(self.el, 'top'); + + createSpinner(self.loader_first, { + radius: 4, + length: 4, + width: 1 + }); + + self.el.setStyle('min-height', 93); + + } + Api.request(self.options.api_call || 'movie.list', { 'data': Object.merge({ 'status': self.options.status, 'limit_offset': self.options.limit + ',' + self.offset }, self.filter), - 'onComplete': function(json){ + 'onSuccess': function(json){ + + if(self.loader_first){ + var lf = self.loader_first; + self.loader_first.addClass('hide') + self.loader_first = null; + setTimeout(function(){ + lf.destroy(); + }, 20000); + self.el.setStyle('min-height', null); + } + self.store(json.movies); self.addMovies(json.movies, json.total); if(self.scrollspy) { @@ -488,7 +516,7 @@ var MovieList = new Class({ self.scrollspy.start(); } - self.checkIfEmpty() + self.checkIfEmpty(); } }); }, @@ -518,7 +546,7 @@ var MovieList = new Class({ self.description[is_empty ? 'hide' : 'show']() if(is_empty && self.options.on_empty_element){ - self.el.grab(self.options.on_empty_element); + self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after'); if(self.navigation) self.navigation.hide(); diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 3e947ddf..c6fe4e0a 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -4,7 +4,33 @@ z-index: 3; } - .movies h2 { + .movies .loading { + display: block; + padding: 20px 0 0 0; + width: 100%; + z-index: 3; + transition: all .4s cubic-bezier(0.9,0,0.1,1); + height: 40px; + opacity: 1; + position: absolute; + text-align: center; + } + .movies .loading.hide { + height: 0; + padding: 20px 0 0 0; + opacity: 0; + margin-top: -20px; + } + + .movies .loading .spinner { + display: inline-block; + } + + .movies .loading .message { + margin: 0 20px; + } + + .movies > h2 { margin-bottom: 20px; } @@ -27,6 +53,7 @@ .home .movies { padding-top: 6px; } + .movies.mass_edit_list { padding-top: 90px; diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index 004f0513..1839e3b0 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -25,7 +25,18 @@ Page.Home = new Class({ 'view': 'list', 'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Refresh, MA.Delete], 'title': 'Snatched & Available', - 'on_empty_element': new Element('div'), + 'description': 'These movies have been snatched or have finished downloading', + 'on_empty_element': new Element('div').adopt( + new Element('h2', {'text': 'Snatched & Available'}), + new Element('span', { + 'html': 'No snatched movies or anything!? Damn.. Maybe add a movie.', + 'events': { + 'click': function(){ + $(document.body).getElement('.search_form input').focus(); + } + } + }) + ), 'filter': { 'release_status': 'snatched,available' } @@ -39,7 +50,7 @@ Page.Home = new Class({ 'title': 'Available soon', 'description': 'These are being searched for and should be available soon as they will be released on DVD in the next few weeks.', 'on_empty_element': new Element('div').adopt( - new Element('h1', {'text': 'Available soon'}), + new Element('h2', {'text': 'Available soon'}), new Element('span', {'text': 'There are no movies available soon. Add some movies, so you have something to watch later.'}) ), 'filter': { @@ -58,10 +69,10 @@ Page.Home = new Class({ 'limit': 50, 'title': 'Still not available', 'description': 'Try another quality profile or maybe add more providers in Settings.', - 'on_empty_element': new Element('div'), 'filter': { 'late': true }, + 'loader': false, 'load_more': false, 'view': 'list', 'actions': [MA.IMDB, MA.Trailer, MA.Edit, MA.Refresh, MA.Delete], From 177063d39cd424a81f3386c9b23e18cd8c1d184a Mon Sep 17 00:00:00 2001 From: Kyle Klein Date: Mon, 8 Apr 2013 14:13:33 -0700 Subject: [PATCH 065/127] Add File Action Move & Sym link --- couchpotato/core/plugins/renamer/__init__.py | 2 +- couchpotato/core/plugins/renamer/main.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index f90828bc..90f5c980 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -117,7 +117,7 @@ config = [{ 'label': 'File Action', 'default': 'move', 'type': 'dropdown', - 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink')], + 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')], 'description': 'Define which kind of file operation you want to use. Before you start using hard links or sym links, PLEASE read about their possible drawbacks.', 'advanced': True, }, diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 4b1fab13..4e3e6ade 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -527,6 +527,9 @@ Remove it if you want it to be renamed (again, or at least let it try again) symlink(old, dest) elif self.conf('file_action') == 'copy': shutil.copy(old, dest) + elif self.conf('file_action') == 'move_symlink': + shutil.move(old, dest) + linktastic.symlink(dest, old) else: shutil.move(old, dest) From cb0b6614c696b90a4db8c99dc8a75a15e1965c05 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 12 Apr 2013 21:14:45 +0200 Subject: [PATCH 066/127] move_symlink proper function --- couchpotato/core/plugins/renamer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 4e3e6ade..87d9aaf3 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -529,7 +529,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) shutil.copy(old, dest) elif self.conf('file_action') == 'move_symlink': shutil.move(old, dest) - linktastic.symlink(dest, old) + symlink(dest, old) else: shutil.move(old, dest) From 64afa3701a78ed652b1d09f9719d26ecf358c264 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 14 Apr 2013 11:08:50 +0200 Subject: [PATCH 067/127] Add headers for IPTorrents. fix #1558 Thanks @got3nks --- couchpotato/core/providers/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 855f3c72..1f0df964 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -104,6 +104,7 @@ class YarrProvider(Provider): try: cookiejar = cookielib.CookieJar() opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar)) + opener.addheaders = [] urllib2.install_opener(opener) log.info2('Logging into %s', self.urls['login']) f = opener.open(self.urls['login'], self.getLoginParams()) From 4f6b31d14ac2c1e29c97cc5eb2e08fd8067f6759 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 16 Apr 2013 21:15:38 +0200 Subject: [PATCH 068/127] Add login check to torrentleech. closes #1635 --- couchpotato/core/providers/torrent/torrentleech/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/couchpotato/core/providers/torrent/torrentleech/main.py b/couchpotato/core/providers/torrent/torrentleech/main.py index 6de18fbd..4247c530 100644 --- a/couchpotato/core/providers/torrent/torrentleech/main.py +++ b/couchpotato/core/providers/torrent/torrentleech/main.py @@ -74,3 +74,6 @@ class TorrentLeech(TorrentProvider): 'remember_me': 'on', 'login': 'submit', }) + + def loginSuccess(self, output): + return '/user/account/logout' in output.lower() or 'welcome back' in output.lower() From 185a530b591bdcfcd5a0343be98638c69efefae7 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sat, 13 Apr 2013 12:45:47 +0200 Subject: [PATCH 069/127] only link for torrents not nzbs --- couchpotato/core/downloaders/base.py | 7 +++++++ couchpotato/core/plugins/renamer/__init__.py | 4 ++-- couchpotato/core/plugins/renamer/main.py | 13 ++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 8ccc1369..a78e0b18 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -39,6 +39,13 @@ class Downloader(Provider): addEvent('download.enabled_types', self.getEnabledDownloadType) addEvent('download.status', self._getAllDownloadStatus) addEvent('download.remove_failed', self._removeFailed) + addEvent('download.downloader_type', self.getDownloaderType) + + def getDownloaderType(self, downloader): + if self.getName() == downloader: + return self.type + + return [] def getEnabledDownloadType(self): for download_type in self.type: diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 90f5c980..f3f7f66c 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -114,11 +114,11 @@ config = [{ }, { 'name': 'file_action', - 'label': 'File Action', + 'label': 'Torrent File Action', 'default': 'move', 'type': 'dropdown', 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')], - 'description': 'Define which kind of file operation you want to use. Before you start using hard links or sym links, PLEASE read about their possible drawbacks.', + 'description': 'Define which kind of file operation you want to use for torrents. Before you start using hard links or sym links, PLEASE read about their possible drawbacks.', 'advanced': True, }, { diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 87d9aaf3..807ee04c 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -118,7 +118,7 @@ class Renamer(Plugin): db = get_session() - # Get the release with the downloader ID that was downloded by the downloader + # Get the release with the downloader ID that was downloaded by the downloader download_info = None if download_id and downloader: rls = None @@ -137,6 +137,7 @@ class Renamer(Plugin): download_info = { 'imdb_id': rls.movie.library.identifier, 'quality': rls.quality.identifier, + 'is_torrent': any(downloader_type in fireEvent('download.downloader_type', downloader = downloader) for downloader_type in ['torrent', 'torrent_magnet']) } else: log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) @@ -446,13 +447,13 @@ class Renamer(Plugin): self.makeDir(os.path.dirname(dst)) try: - self.moveFile(src, dst) + self.moveFile(src, dst, forcemove = not (download_info and download_info.get('is_torrent'))) group['renamed_files'].append(dst) except: log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) self.tagDir(group, 'failed_rename') - if self.conf('file_action') != 'move': + if self.conf('file_action') != 'move' and download_info and download_info.get('is_torrent'): self.tagDir(group, 'renamed already') # Remove matching releases @@ -518,10 +519,12 @@ Remove it if you want it to be renamed (again, or at least let it try again) self.createFile(ignore_file, text) - def moveFile(self, old, dest): + def moveFile(self, old, dest, forcemove = False): dest = ss(dest) try: - if self.conf('file_action') == 'hardlink': + if forcemove: + shutil.move(old, dest) + elif self.conf('file_action') == 'hardlink': link(old, dest) elif self.conf('file_action') == 'symlink': symlink(old, dest) From 0e90739786dd55a8cd7c719674973977f335ed5a Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 19 Apr 2013 14:49:27 +0200 Subject: [PATCH 070/127] Update to Tornado 3.0 --- couchpotato/core/_base/_core/main.py | 6 +- couchpotato/runner.py | 3 +- libs/tornado/__init__.py | 8 +- libs/tornado/auth.py | 472 ++++++++++++++----------- libs/tornado/autoreload.py | 31 +- libs/tornado/concurrent.py | 115 ++++-- libs/tornado/curl_httpclient.py | 18 +- libs/tornado/escape.py | 31 +- libs/tornado/gen.py | 204 +++++++++-- libs/tornado/httpclient.py | 177 +++++----- libs/tornado/httpserver.py | 162 +++++---- libs/tornado/httputil.py | 46 ++- libs/tornado/ioloop.py | 241 ++++++++----- libs/tornado/iostream.py | 102 +++--- libs/tornado/locale.py | 80 +++-- libs/tornado/netutil.py | 123 +++++-- libs/tornado/options.py | 64 ++-- libs/tornado/platform/caresresolver.py | 75 ++++ libs/tornado/platform/twisted.py | 102 +++++- libs/tornado/process.py | 16 +- libs/tornado/simple_httpclient.py | 65 ++-- libs/tornado/stack_context.py | 220 ++++++------ libs/tornado/tcpserver.py | 24 +- libs/tornado/template.py | 25 +- libs/tornado/testing.py | 222 +++++++----- libs/tornado/util.py | 50 ++- libs/tornado/web.py | 280 ++++++++------- libs/tornado/websocket.py | 108 +++--- libs/tornado/wsgi.py | 28 +- 29 files changed, 1932 insertions(+), 1166 deletions(-) create mode 100755 libs/tornado/platform/caresresolver.py diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index c91140fa..d2b6e2d3 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -79,7 +79,7 @@ class Core(Plugin): def shutdown(): self.initShutdown() - IOLoop.instance().add_callback(shutdown) + IOLoop.current().add_callback(shutdown) return 'shutdown' @@ -89,7 +89,7 @@ class Core(Plugin): def restart(): self.initShutdown(restart = True) - IOLoop.instance().add_callback(restart) + IOLoop.current().add_callback(restart) return 'restarting' @@ -128,7 +128,7 @@ class Core(Plugin): log.debug('Save to shutdown/restart') try: - IOLoop.instance().stop() + IOLoop.current().stop() except RuntimeError: pass except: diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 0ae14ce0..26522178 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -241,7 +241,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En from tornado.ioloop import IOLoop web_container = WSGIContainer(app) web_container._log = _log - loop = IOLoop.instance() + loop = IOLoop.current() + application = Application([ (r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler), diff --git a/libs/tornado/__init__.py b/libs/tornado/__init__.py index 609e2c05..68434e18 100755 --- a/libs/tornado/__init__.py +++ b/libs/tornado/__init__.py @@ -23,7 +23,7 @@ from __future__ import absolute_import, division, print_function, with_statement # version_info is a four-tuple for programmatic comparison. The first # three numbers are the components of the version number. The fourth # is zero for an official release, positive for a development branch, -# or negative for a release candidate (after the base version number -# has been incremented) -version = "2.4.post3" -version_info = (2, 4, 0, 3) +# or negative for a release candidate or beta (after the base version +# number has been incremented) +version = "3.1.dev2" +version_info = (3, 1, 0, -99) diff --git a/libs/tornado/auth.py b/libs/tornado/auth.py index 0ff32cb2..df95884b 100755 --- a/libs/tornado/auth.py +++ b/libs/tornado/auth.py @@ -14,15 +14,19 @@ # License for the specific language governing permissions and limitations # under the License. -"""Implementations of various third-party authentication schemes. +"""This module contains implementations of various third-party +authentication schemes. -All the classes in this file are class Mixins designed to be used with -web.py RequestHandler classes. The primary methods for each service are -authenticate_redirect(), authorize_redirect(), and get_authenticated_user(). -The former should be called to redirect the user to, e.g., the OpenID -authentication page on the third party service, and the latter should -be called upon return to get the user data from the data returned by -the third party service. +All the classes in this file are class mixins designed to be used with +the `tornado.web.RequestHandler` class. They are used in two ways: + +* On a login handler, use methods such as ``authenticate_redirect()``, + ``authorize_redirect()``, and ``get_authenticated_user()`` to + establish the user's identity and store authentication tokens to your + database and/or cookies. +* In non-login handlers, use methods such as ``facebook_request()`` + or ``twitter_request()`` to use the authentication tokens to make + requests to the respective services. They all take slightly different arguments due to the fact all these services implement authentication and authorization slightly differently. @@ -30,18 +34,16 @@ See the individual service classes below for complete documentation. Example usage for Google OpenID:: - class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin): + class GoogleLoginHandler(tornado.web.RequestHandler, + tornado.auth.GoogleMixin): @tornado.web.asynchronous + @tornado.gen.coroutine def get(self): if self.get_argument("openid.mode", None): - self.get_authenticated_user(self.async_callback(self._on_auth)) - return - self.authenticate_redirect() - - def _on_auth(self, user): - if not user: - raise tornado.web.HTTPError(500, "Google auth failed") - # Save the user with, e.g., set_secure_cookie() + user = yield self.get_authenticated_user() + # Save the user with e.g. set_secure_cookie() + else: + self.authenticate_redirect() """ from __future__ import absolute_import, division, print_function, with_statement @@ -72,9 +74,11 @@ try: except ImportError: import urllib as urllib_parse # py2 + class AuthError(Exception): pass + def _auth_future_to_callback(callback, future): try: result = future.result() @@ -83,6 +87,7 @@ def _auth_future_to_callback(callback, future): result = None callback(result) + def _auth_return_future(f): """Similar to tornado.concurrent.return_future, but uses the auth module's legacy callback interface. @@ -91,6 +96,7 @@ def _auth_return_future(f): inside the function will actually be a future. """ replacer = ArgReplacer(f, 'callback') + @functools.wraps(f) def wrapper(*args, **kwargs): future = Future() @@ -102,17 +108,23 @@ def _auth_return_future(f): return future return wrapper + class OpenIdMixin(object): """Abstract implementation of OpenID and Attribute Exchange. - See GoogleMixin below for example implementations. + See `GoogleMixin` below for a customized example (which also + includes OAuth support). + + Class attributes: + + * ``_OPENID_ENDPOINT``: the identity provider's URI. """ def authenticate_redirect(self, callback_uri=None, ax_attrs=["name", "email", "language", "username"]): - """Returns the authentication URL for this service. + """Redirects to the authentication URL for this service. After authentication, the service will redirect back to the given - callback URI. + callback URI with additional parameters including ``openid.mode``. We request the given attributes for the authenticated user by default (name, email, language, and username). If you don't need @@ -128,8 +140,12 @@ class OpenIdMixin(object): """Fetches the authenticated user data upon redirect. This method should be called by the handler that receives the - redirect from the authenticate_redirect() or authorize_redirect() - methods. + redirect from the `authenticate_redirect()` method (which is + often the same as the one that calls it; in that case you would + call `get_authenticated_user` if the ``openid.mode`` parameter + is present and `authenticate_redirect` if it is not). + + The result of this method will generally be used to set a cookie. """ # Verify the OpenID response via direct request to the OP args = dict((k, v[-1]) for k, v in self.request.arguments.items()) @@ -192,8 +208,8 @@ class OpenIdMixin(object): def _on_authentication_verified(self, future, response): if response.error or b"is_valid:true" not in response.body: future.set_exception(AuthError( - "Invalid OpenID response: %s" % (response.error or - response.body))) + "Invalid OpenID response: %s" % (response.error or + response.body))) return # Make sure we got back at least an email from attribute exchange @@ -250,32 +266,44 @@ class OpenIdMixin(object): future.set_result(user) def get_auth_http_client(self): - """Returns the AsyncHTTPClient instance to be used for auth requests. + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. - May be overridden by subclasses to use an http client other than + May be overridden by subclasses to use an HTTP client other than the default. """ return httpclient.AsyncHTTPClient() class OAuthMixin(object): - """Abstract implementation of OAuth. + """Abstract implementation of OAuth 1.0 and 1.0a. - See TwitterMixin and FriendFeedMixin below for example implementations. + See `TwitterMixin` and `FriendFeedMixin` below for example implementations, + or `GoogleMixin` for an OAuth/OpenID hybrid. + + Class attributes: + + * ``_OAUTH_AUTHORIZE_URL``: The service's OAuth authorization url. + * ``_OAUTH_ACCESS_TOKEN_URL``: The service's OAuth access token url. + * ``_OAUTH_VERSION``: May be either "1.0" or "1.0a". + * ``_OAUTH_NO_CALLBACKS``: Set this to True if the service requires + advance registration of callbacks. + + Subclasses must also override the `_oauth_get_user_future` and + `_oauth_consumer_token` methods. """ def authorize_redirect(self, callback_uri=None, extra_params=None, http_client=None): """Redirects the user to obtain OAuth authorization for this service. - Twitter and FriendFeed both require that you register a Callback - URL with your application. You should call this method to log the - user in, and then call get_authenticated_user() in the handler - you registered as your Callback URL to complete the authorization - process. + The ``callback_uri`` may be omitted if you have previously + registered a callback URI with the third-party service. For some + sevices (including Twitter and Friendfeed), you must use a + previously-registered callback URI and cannot specify a callback + via this method. - This method sets a cookie called _oauth_request_token which is - subsequently used (and cleared) in get_authenticated_user for + This method sets a cookie called ``_oauth_request_token`` which is + subsequently used (and cleared) in `get_authenticated_user` for security purposes. """ if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False): @@ -299,15 +327,15 @@ class OAuthMixin(object): @_auth_return_future def get_authenticated_user(self, callback, http_client=None): - """Gets the OAuth authorized user and access token on callback. - - This method should be called from the handler for your registered - OAuth Callback URL to complete the registration process. We call - callback with the authenticated user, which in addition to standard - attributes like 'name' includes the 'access_key' attribute, which - contains the OAuth access you can use to make authorized requests - to this service on behalf of the user. + """Gets the OAuth authorized user and access token. + This method should be called from the handler for your + OAuth callback URL to complete the registration process. We run the + callback with the authenticated user dictionary. This dictionary + will contain an ``access_key`` which can be used to make authorized + requests to this service on behalf of the user. The dictionary will + also contain other fields such as ``name``, depending on the service + used. """ future = callback request_key = escape.utf8(self.get_argument("oauth_token")) @@ -315,13 +343,13 @@ class OAuthMixin(object): request_cookie = self.get_cookie("_oauth_request_token") if not request_cookie: future.set_exception(AuthError( - "Missing OAuth request token cookie")) + "Missing OAuth request token cookie")) return self.clear_cookie("_oauth_request_token") cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")] if cookie_key != request_key: future.set_exception(AuthError( - "Request token does not match cookie")) + "Request token does not match cookie")) return token = dict(key=cookie_key, secret=cookie_secret) if oauth_verifier: @@ -339,7 +367,7 @@ class OAuthMixin(object): oauth_signature_method="HMAC-SHA1", oauth_timestamp=str(int(time.time())), oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), - oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), + oauth_version="1.0", ) if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": if callback_uri == "oob": @@ -360,8 +388,8 @@ class OAuthMixin(object): if response.error: raise Exception("Could not get request token") request_token = _oauth_parse_response(response.body) - data = (base64.b64encode(request_token["key"]) + b"|" + - base64.b64encode(request_token["secret"])) + data = (base64.b64encode(escape.utf8(request_token["key"])) + b"|" + + base64.b64encode(escape.utf8(request_token["secret"]))) self.set_cookie("_oauth_request_token", data) args = dict(oauth_token=request_token["key"]) if callback_uri == "oob": @@ -381,7 +409,7 @@ class OAuthMixin(object): oauth_signature_method="HMAC-SHA1", oauth_timestamp=str(int(time.time())), oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), - oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), + oauth_version="1.0", ) if "verifier" in request_token: args["oauth_verifier"] = request_token["verifier"] @@ -405,8 +433,29 @@ class OAuthMixin(object): self._oauth_get_user_future(access_token).add_done_callback( self.async_callback(self._on_oauth_get_user, access_token, future)) + def _oauth_consumer_token(self): + """Subclasses must override this to return their OAuth consumer keys. + + The return value should be a `dict` with keys ``key`` and ``secret``. + """ + raise NotImplementedError() + @return_future def _oauth_get_user_future(self, access_token, callback): + """Subclasses must override this to get basic information about the + user. + + Should return a `.Future` whose result is a dictionary + containing information about the user, which may have been + retrieved by using ``access_token`` to make a request to the + service. + + The access token will be added to the returned dictionary to make + the result of `get_authenticated_user`. + + For backwards compatibility, the callback-based ``_oauth_get_user`` + method is also supported. + """ # By default, call the old-style _oauth_get_user, but new code # should override this method instead. self._oauth_get_user(access_token, callback) @@ -439,7 +488,7 @@ class OAuthMixin(object): oauth_signature_method="HMAC-SHA1", oauth_timestamp=str(int(time.time())), oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)), - oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"), + oauth_version="1.0", ) args = {} args.update(base_args) @@ -450,30 +499,38 @@ class OAuthMixin(object): else: signature = _oauth_signature(consumer_token, method, url, args, access_token) - base_args["oauth_signature"] = signature + base_args["oauth_signature"] = escape.to_basestring(signature) return base_args def get_auth_http_client(self): - """Returns the AsyncHTTPClient instance to be used for auth requests. + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. - May be overridden by subclasses to use an http client other than + May be overridden by subclasses to use an HTTP client other than the default. """ return httpclient.AsyncHTTPClient() class OAuth2Mixin(object): - """Abstract implementation of OAuth v 2.""" + """Abstract implementation of OAuth 2.0. + + See `FacebookGraphMixin` below for an example implementation. + + Class attributes: + + * ``_OAUTH_AUTHORIZE_URL``: The service's authorization url. + * ``_OAUTH_ACCESS_TOKEN_URL``: The service's access token url. + """ def authorize_redirect(self, redirect_uri=None, client_id=None, client_secret=None, extra_params=None): """Redirects the user to obtain OAuth authorization for this service. - Some providers require that you register a Callback - URL with your application. You should call this method to log the - user in, and then call get_authenticated_user() in the handler - you registered as your Callback URL to complete the authorization - process. + Some providers require that you register a redirect URL with + your application instead of passing one via this method. You + should call this method to log the user in, and then call + ``get_authenticated_user`` in the handler for your + redirect URL to complete the authorization process. """ args = { "redirect_uri": redirect_uri, @@ -503,35 +560,30 @@ class TwitterMixin(OAuthMixin): """Twitter OAuth authentication. To authenticate with Twitter, register your application with - Twitter at http://twitter.com/apps. Then copy your Consumer Key and - Consumer Secret to the application settings 'twitter_consumer_key' and - 'twitter_consumer_secret'. Use this Mixin on the handler for the URL - you registered as your application's Callback URL. + Twitter at http://twitter.com/apps. Then copy your Consumer Key + and Consumer Secret to the application + `~tornado.web.Application.settings` ``twitter_consumer_key`` and + ``twitter_consumer_secret``. Use this mixin on the handler for the + URL you registered as your application's callback URL. - When your application is set up, you can use this Mixin like this + When your application is set up, you can use this mixin like this to authenticate the user with Twitter and get access to their stream:: - class TwitterHandler(tornado.web.RequestHandler, - tornado.auth.TwitterMixin): + class TwitterLoginHandler(tornado.web.RequestHandler, + tornado.auth.TwitterMixin): @tornado.web.asynchronous + @tornado.gen.coroutine def get(self): if self.get_argument("oauth_token", None): - self.get_authenticated_user(self.async_callback(self._on_auth)) - return - self.authorize_redirect() + user = yield self.get_authenticated_user() + # Save the user using e.g. set_secure_cookie() + else: + self.authorize_redirect() - def _on_auth(self, user): - if not user: - raise tornado.web.HTTPError(500, "Twitter auth failed") - # Save the user using, e.g., set_secure_cookie() - - The user object returned by get_authenticated_user() includes the - attributes 'username', 'name', and all of the custom Twitter user - attributes describe at - http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show - in addition to 'access_token'. You should save the access token with - the user; it is required to make requests on behalf of the user later - with twitter_request(). + The user object returned by `~OAuthMixin.get_authenticated_user` + includes the attributes ``username``, ``name``, ``access_token``, + and all of the custom Twitter user attributes described at + https://dev.twitter.com/docs/api/1.1/get/users/show """ _OAUTH_REQUEST_TOKEN_URL = "http://api.twitter.com/oauth/request_token" _OAUTH_ACCESS_TOKEN_URL = "http://api.twitter.com/oauth/access_token" @@ -541,7 +593,8 @@ class TwitterMixin(OAuthMixin): _TWITTER_BASE_URL = "http://api.twitter.com/1" def authenticate_redirect(self, callback_uri=None): - """Just like authorize_redirect(), but auto-redirects if authorized. + """Just like `~OAuthMixin.authorize_redirect`, but + auto-redirects if authorized. This is generally the right interface to use if you are using Twitter for single-sign on. @@ -553,35 +606,33 @@ class TwitterMixin(OAuthMixin): @_auth_return_future def twitter_request(self, path, callback=None, access_token=None, post_args=None, **args): - """Fetches the given API path, e.g., "/statuses/user_timeline/btaylor" + """Fetches the given API path, e.g., ``/statuses/user_timeline/btaylor`` - The path should not include the format (we automatically append - ".json" and parse the JSON output). + The path should not include the format or API version number. + (we automatically use JSON format and API version 1). - If the request is a POST, post_args should be provided. Query + If the request is a POST, ``post_args`` should be provided. Query string arguments should be given as keyword arguments. - All the Twitter methods are documented at - http://apiwiki.twitter.com/Twitter-API-Documentation. + All the Twitter methods are documented at http://dev.twitter.com/ - Many methods require an OAuth access token which you can obtain - through authorize_redirect() and get_authenticated_user(). The - user returned through that process includes an 'access_token' - attribute that can be used to make authenticated requests via - this method. Example usage:: + Many methods require an OAuth access token which you can + obtain through `~OAuthMixin.authorize_redirect` and + `~OAuthMixin.get_authenticated_user`. The user returned through that + process includes an 'access_token' attribute that can be used + to make authenticated requests via this method. Example + usage:: class MainHandler(tornado.web.RequestHandler, tornado.auth.TwitterMixin): @tornado.web.authenticated @tornado.web.asynchronous + @tornado.gen.coroutine def get(self): - self.twitter_request( + new_entry = yield self.twitter_request( "/statuses/update", post_args={"status": "Testing Tornado Web Server"}, - access_token=user["access_token"], - callback=self.async_callback(self._on_post)) - - def _on_post(self, new_entry): + access_token=self.current_user["access_token"]) if not new_entry: # Call failed; perhaps missing permission? self.authorize_redirect() @@ -617,8 +668,8 @@ class TwitterMixin(OAuthMixin): def _on_twitter_request(self, future, response): if response.error: future.set_exception(AuthError( - "Error response %s fetching %s" % (response.error, - response.request.url))) + "Error response %s fetching %s" % (response.error, + response.request.url))) return future.set_result(escape.json_decode(response.body)) @@ -629,49 +680,45 @@ class TwitterMixin(OAuthMixin): key=self.settings["twitter_consumer_key"], secret=self.settings["twitter_consumer_secret"]) - @return_future - @gen.engine - def _oauth_get_user_future(self, access_token, callback): + @gen.coroutine + def _oauth_get_user_future(self, access_token): user = yield self.twitter_request( - "/users/show/" + escape.native_str(access_token[b"screen_name"]), + "/users/show/" + escape.native_str(access_token["screen_name"]), access_token=access_token) if user: user["username"] = user["screen_name"] - callback(user) + raise gen.Return(user) class FriendFeedMixin(OAuthMixin): """FriendFeed OAuth authentication. To authenticate with FriendFeed, register your application with - FriendFeed at http://friendfeed.com/api/applications. Then - copy your Consumer Key and Consumer Secret to the application settings - 'friendfeed_consumer_key' and 'friendfeed_consumer_secret'. Use - this Mixin on the handler for the URL you registered as your - application's Callback URL. + FriendFeed at http://friendfeed.com/api/applications. Then copy + your Consumer Key and Consumer Secret to the application + `~tornado.web.Application.settings` ``friendfeed_consumer_key`` + and ``friendfeed_consumer_secret``. Use this mixin on the handler + for the URL you registered as your application's Callback URL. - When your application is set up, you can use this Mixin like this + When your application is set up, you can use this mixin like this to authenticate the user with FriendFeed and get access to their feed:: - class FriendFeedHandler(tornado.web.RequestHandler, - tornado.auth.FriendFeedMixin): + class FriendFeedLoginHandler(tornado.web.RequestHandler, + tornado.auth.FriendFeedMixin): @tornado.web.asynchronous + @tornado.gen.coroutine def get(self): if self.get_argument("oauth_token", None): - self.get_authenticated_user(self.async_callback(self._on_auth)) - return - self.authorize_redirect() + user = yield self.get_authenticated_user() + # Save the user using e.g. set_secure_cookie() + else: + self.authorize_redirect() - def _on_auth(self, user): - if not user: - raise tornado.web.HTTPError(500, "FriendFeed auth failed") - # Save the user using, e.g., set_secure_cookie() - - The user object returned by get_authenticated_user() includes the - attributes 'username', 'name', and 'description' in addition to - 'access_token'. You should save the access token with the user; + The user object returned by `~OAuthMixin.get_authenticated_user()` includes the + attributes ``username``, ``name``, and ``description`` in addition to + ``access_token``. You should save the access token with the user; it is required to make requests on behalf of the user later with - friendfeed_request(). + `friendfeed_request()`. """ _OAUTH_VERSION = "1.0" _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token" @@ -685,30 +732,32 @@ class FriendFeedMixin(OAuthMixin): post_args=None, **args): """Fetches the given relative API path, e.g., "/bret/friends" - If the request is a POST, post_args should be provided. Query + If the request is a POST, ``post_args`` should be provided. Query string arguments should be given as keyword arguments. All the FriendFeed methods are documented at http://friendfeed.com/api/documentation. - Many methods require an OAuth access token which you can obtain - through authorize_redirect() and get_authenticated_user(). The - user returned through that process includes an 'access_token' - attribute that can be used to make authenticated requests via - this method. Example usage:: + Many methods require an OAuth access token which you can + obtain through `~OAuthMixin.authorize_redirect` and + `~OAuthMixin.get_authenticated_user`. The user returned + through that process includes an ``access_token`` attribute that + can be used to make authenticated requests via this + method. + + Example usage:: class MainHandler(tornado.web.RequestHandler, tornado.auth.FriendFeedMixin): @tornado.web.authenticated @tornado.web.asynchronous + @tornado.gen.coroutine def get(self): - self.friendfeed_request( + new_entry = yield self.friendfeed_request( "/entry", post_args={"body": "Testing Tornado Web Server"}, - access_token=self.current_user["access_token"], - callback=self.async_callback(self._on_post)) + access_token=self.current_user["access_token"]) - def _on_post(self, new_entry): if not new_entry: # Call failed; perhaps missing permission? self.authorize_redirect() @@ -739,8 +788,8 @@ class FriendFeedMixin(OAuthMixin): def _on_friendfeed_request(self, future, response): if response.error: future.set_exception(AuthError( - "Error response %s fetching %s" % (response.error, - response.request.url))) + "Error response %s fetching %s" % (response.error, + response.request.url))) return future.set_result(escape.json_decode(response.body)) @@ -751,9 +800,8 @@ class FriendFeedMixin(OAuthMixin): key=self.settings["friendfeed_consumer_key"], secret=self.settings["friendfeed_consumer_secret"]) - @return_future - @gen.engine - def _oauth_get_user(self, access_token, callback): + @gen.coroutine + def _oauth_get_user_future(self, access_token, callback): user = yield self.friendfeed_request( "/feedinfo/" + access_token["username"], include="id,name,description", access_token=access_token) @@ -770,26 +818,30 @@ class FriendFeedMixin(OAuthMixin): class GoogleMixin(OpenIdMixin, OAuthMixin): """Google Open ID / OAuth authentication. - No application registration is necessary to use Google for authentication - or to access Google resources on behalf of a user. To authenticate with - Google, redirect with authenticate_redirect(). On return, parse the - response with get_authenticated_user(). We send a dict containing the - values for the user, including 'email', 'name', and 'locale'. + No application registration is necessary to use Google for + authentication or to access Google resources on behalf of a user. + + Google implements both OpenID and OAuth in a hybrid mode. If you + just need the user's identity, use + `~OpenIdMixin.authenticate_redirect`. If you need to make + requests to Google on behalf of the user, use + `authorize_redirect`. On return, parse the response with + `~OpenIdMixin.get_authenticated_user`. We send a dict containing + the values for the user, including ``email``, ``name``, and + ``locale``. + Example usage:: - class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin): + class GoogleLoginHandler(tornado.web.RequestHandler, + tornado.auth.GoogleMixin): @tornado.web.asynchronous + @tornado.gen.coroutine def get(self): if self.get_argument("openid.mode", None): - self.get_authenticated_user(self.async_callback(self._on_auth)) - return - self.authenticate_redirect() - - def _on_auth(self, user): - if not user: - raise tornado.web.HTTPError(500, "Google auth failed") - # Save the user with, e.g., set_secure_cookie() - + user = yield self.get_authenticated_user() + # Save the user with e.g. set_secure_cookie() + else: + self.authenticate_redirect() """ _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud" _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken" @@ -798,7 +850,8 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): ax_attrs=["name", "email", "language", "username"]): """Authenticates and authorizes for the given Google resource. - Some of the available resources are: + Some of the available resources which can be used in the ``oauth_scope`` + argument are: * Gmail Contacts - http://www.google.com/m8/feeds/ * Calendar - http://www.google.com/calendar/feeds/ @@ -839,7 +892,7 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): key=self.settings["google_consumer_key"], secret=self.settings["google_consumer_secret"]) - def _oauth_get_user_future(self, access_token, callback): + def _oauth_get_user_future(self, access_token): return OpenIdMixin.get_authenticated_user(self) @@ -853,9 +906,9 @@ class FacebookMixin(object): To authenticate with Facebook, register your application with Facebook at http://www.facebook.com/developers/apps.php. Then copy your API Key and Application Secret to the application settings - 'facebook_api_key' and 'facebook_secret'. + ``facebook_api_key`` and ``facebook_secret``. - When your application is set up, you can use this Mixin like this + When your application is set up, you can use this mixin like this to authenticate the user with Facebook:: class FacebookHandler(tornado.web.RequestHandler, @@ -872,11 +925,11 @@ class FacebookMixin(object): raise tornado.web.HTTPError(500, "Facebook auth failed") # Save the user using, e.g., set_secure_cookie() - The user object returned by get_authenticated_user() includes the - attributes 'facebook_uid' and 'name' in addition to session attributes - like 'session_key'. You should save the session key with the user; it is + The user object returned by `get_authenticated_user` includes the + attributes ``facebook_uid`` and ``name`` in addition to session attributes + like ``session_key``. You should save the session key with the user; it is required to make requests on behalf of the user later with - facebook_request(). + `facebook_request`. """ def authenticate_redirect(self, callback_uri=None, cancel_uri=None, extended_permissions=None): @@ -1029,9 +1082,9 @@ class FacebookMixin(object): return hashlib.md5(body).hexdigest() def get_auth_http_client(self): - """Returns the AsyncHTTPClient instance to be used for auth requests. + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. - May be overridden by subclasses to use an http client other than + May be overridden by subclasses to use an HTTP client other than the default. """ return httpclient.AsyncHTTPClient() @@ -1043,6 +1096,7 @@ class FacebookGraphMixin(OAuth2Mixin): _OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?" _OAUTH_NO_CALLBACKS = False + @_auth_return_future def get_authenticated_user(self, redirect_uri, client_id, client_secret, code, callback, extra_fields=None): """Handles the login for the Facebook user, returning a user object. @@ -1051,24 +1105,20 @@ class FacebookGraphMixin(OAuth2Mixin): class FacebookGraphLoginHandler(LoginHandler, tornado.auth.FacebookGraphMixin): @tornado.web.asynchronous + @tornado.gen.coroutine def get(self): if self.get_argument("code", False): - self.get_authenticated_user( - redirect_uri='/auth/facebookgraph/', - client_id=self.settings["facebook_api_key"], - client_secret=self.settings["facebook_secret"], - code=self.get_argument("code"), - callback=self.async_callback( - self._on_login)) - return - self.authorize_redirect(redirect_uri='/auth/facebookgraph/', - client_id=self.settings["facebook_api_key"], - extra_params={"scope": "read_stream,offline_access"}) - - def _on_login(self, user): - logging.error(user) - self.finish() - + user = yield self.get_authenticated_user( + redirect_uri='/auth/facebookgraph/', + client_id=self.settings["facebook_api_key"], + client_secret=self.settings["facebook_secret"], + code=self.get_argument("code")) + # Save the user with e.g. set_secure_cookie + else: + self.authorize_redirect( + redirect_uri='/auth/facebookgraph/', + client_id=self.settings["facebook_api_key"], + extra_params={"scope": "read_stream,offline_access"}) """ http = self.get_auth_http_client() args = { @@ -1088,10 +1138,9 @@ class FacebookGraphMixin(OAuth2Mixin): client_secret, callback, fields)) def _on_access_token(self, redirect_uri, client_id, client_secret, - callback, fields, response): + future, fields, response): if response.error: - gen_log.warning('Facebook auth error: %s' % str(response)) - callback(None) + future.set_exception(AuthError('Facebook auth error: %s' % str(response))) return args = escape.parse_qs_bytes(escape.native_str(response.body)) @@ -1103,14 +1152,14 @@ class FacebookGraphMixin(OAuth2Mixin): self.facebook_request( path="/me", callback=self.async_callback( - self._on_get_user_info, callback, session, fields), + self._on_get_user_info, future, session, fields), access_token=session["access_token"], fields=",".join(fields) ) - def _on_get_user_info(self, callback, session, fields, user): + def _on_get_user_info(self, future, session, fields, user): if user is None: - callback(None) + future.set_result(None) return fieldmap = {} @@ -1118,42 +1167,43 @@ class FacebookGraphMixin(OAuth2Mixin): fieldmap[field] = user.get(field) fieldmap.update({"access_token": session["access_token"], "session_expires": session.get("expires")}) - callback(fieldmap) + future.set_result(fieldmap) + @_auth_return_future def facebook_request(self, path, callback, access_token=None, post_args=None, **args): """Fetches the given relative API path, e.g., "/btaylor/picture" - If the request is a POST, post_args should be provided. Query + If the request is a POST, ``post_args`` should be provided. Query string arguments should be given as keyword arguments. An introduction to the Facebook Graph API can be found at http://developers.facebook.com/docs/api - Many methods require an OAuth access token which you can obtain - through authorize_redirect() and get_authenticated_user(). The - user returned through that process includes an 'access_token' - attribute that can be used to make authenticated requests via - this method. Example usage:: + Many methods require an OAuth access token which you can + obtain through `~OAuth2Mixin.authorize_redirect` and + `get_authenticated_user`. The user returned through that + process includes an ``access_token`` attribute that can be + used to make authenticated requests via this method. + + Example usage:: class MainHandler(tornado.web.RequestHandler, tornado.auth.FacebookGraphMixin): @tornado.web.authenticated @tornado.web.asynchronous + @tornado.gen.coroutine def get(self): - self.facebook_request( + new_entry = yield self.facebook_request( "/me/feed", post_args={"message": "I am posting from my Tornado application!"}, - access_token=self.current_user["access_token"], - callback=self.async_callback(self._on_post)) + access_token=self.current_user["access_token"]) - def _on_post(self, new_entry): if not new_entry: # Call failed; perhaps missing permission? self.authorize_redirect() return self.finish("Posted a message!") - """ url = "https://graph.facebook.com" + path all_args = {} @@ -1171,18 +1221,18 @@ class FacebookGraphMixin(OAuth2Mixin): else: http.fetch(url, callback=callback) - def _on_facebook_request(self, callback, response): + def _on_facebook_request(self, future, response): if response.error: - gen_log.warning("Error response %s fetching %s", response.error, - response.request.url) - callback(None) + future.set_exception(AuthError("Error response %s fetching %s", + response.error, response.request.url)) return - callback(escape.json_decode(response.body)) + + future.set_result(escape.json_decode(response.body)) def get_auth_http_client(self): - """Returns the AsyncHTTPClient instance to be used for auth requests. + """Returns the `.AsyncHTTPClient` instance to be used for auth requests. - May be overridden by subclasses to use an http client other than + May be overridden by subclasses to use an HTTP client other than the default. """ return httpclient.AsyncHTTPClient() @@ -1243,10 +1293,14 @@ def _oauth_escape(val): def _oauth_parse_response(body): - p = escape.parse_qs(body, keep_blank_values=False) - token = dict(key=p[b"oauth_token"][0], secret=p[b"oauth_token_secret"][0]) + # I can't find an officially-defined encoding for oauth responses and + # have never seen anyone use non-ascii. Leave the response in a byte + # string for python 2, and use utf8 on python 3. + body = escape.native_str(body) + p = urlparse.parse_qs(body, keep_blank_values=False) + token = dict(key=p["oauth_token"][0], secret=p["oauth_token_secret"][0]) # Add the extra parameters the Provider included to the token - special = (b"oauth_token", b"oauth_token_secret") + special = ("oauth_token", "oauth_token_secret") token.update((k, p[k][0]) for k in p if k not in special) return token diff --git a/libs/tornado/autoreload.py b/libs/tornado/autoreload.py index 4e424878..05754299 100755 --- a/libs/tornado/autoreload.py +++ b/libs/tornado/autoreload.py @@ -14,15 +14,24 @@ # License for the specific language governing permissions and limitations # under the License. -"""A module to automatically restart the server when a module is modified. +"""xAutomatically restart the server when a source file is modified. -Most applications should not call this module directly. Instead, pass the +Most applications should not access this module directly. Instead, pass the keyword argument ``debug=True`` to the `tornado.web.Application` constructor. This will enable autoreload mode as well as checking for changes to templates -and static resources. +and static resources. Note that restarting is a destructive operation +and any requests in progress will be aborted when the process restarts. -This module depends on IOLoop, so it will not work in WSGI applications -and Google AppEngine. It also will not work correctly when HTTPServer's +This module can also be used as a command-line wrapper around scripts +such as unit test runners. See the `main` method for details. + +The command-line wrapper and Application debug modes can be used together. +This combination is encouraged as the wrapper catches syntax errors and +other import-time failures, while debug mode catches changes once +the server has started. + +This module depends on `.IOLoop`, so it will not work in WSGI applications +and Google App Engine. It also will not work correctly when `.HTTPServer`'s multi-process mode is used. Reloading loses any Python interpreter command-line arguments (e.g. ``-u``) @@ -94,12 +103,8 @@ _io_loops = weakref.WeakKeyDictionary() def start(io_loop=None, check_time=500): - """Restarts the process automatically when a module is modified. - - We run on the I/O loop, and restarting is a destructive operation, - so will terminate any pending requests. - """ - io_loop = io_loop or ioloop.IOLoop.instance() + """Begins watching source files for changes using the given `.IOLoop`. """ + io_loop = io_loop or ioloop.IOLoop.current() if io_loop in _io_loops: return _io_loops[io_loop] = True @@ -137,8 +142,8 @@ def add_reload_hook(fn): Note that for open file and socket handles it is generally preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or - `tornado.platform.auto.set_close_exec`) instead of using a reload - hook to close them. + ``tornado.platform.auto.set_close_exec``) instead + of using a reload hook to close them. """ _reload_hooks.append(fn) diff --git a/libs/tornado/concurrent.py b/libs/tornado/concurrent.py index 59075a3a..15a039ca 100755 --- a/libs/tornado/concurrent.py +++ b/libs/tornado/concurrent.py @@ -13,12 +13,21 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +"""Utilities for working with threads and ``Futures``. + +``Futures`` are a pattern for concurrent programming introduced in +Python 3.2 in the `concurrent.futures` package (this package has also +been backported to older versions of Python and can be installed with +``pip install futures``). Tornado will use `concurrent.futures.Future` if +it is available; otherwise it will use a compatible class defined in this +module. +""" from __future__ import absolute_import, division, print_function, with_statement import functools import sys -from tornado.stack_context import ExceptionStackContext +from tornado.stack_context import ExceptionStackContext, wrap from tornado.util import raise_exc_info, ArgReplacer try: @@ -26,10 +35,12 @@ try: except ImportError: futures = None + class ReturnValueIgnoredError(Exception): pass -class DummyFuture(object): + +class _DummyFuture(object): def __init__(self): self._done = False self._result = None @@ -87,50 +98,92 @@ class DummyFuture(object): self._callbacks = None if futures is None: - Future = DummyFuture + Future = _DummyFuture else: Future = futures.Future +class TracebackFuture(Future): + """Subclass of `Future` which can store a traceback with + exceptions. + + The traceback is automatically available in Python 3, but in the + Python 2 futures backport this information is discarded. + """ + def __init__(self): + super(TracebackFuture, self).__init__() + self.__exc_info = None + + def exc_info(self): + return self.__exc_info + + def set_exc_info(self, exc_info): + """Traceback-aware replacement for + `~concurrent.futures.Future.set_exception`. + """ + self.__exc_info = exc_info + self.set_exception(exc_info[1]) + + def result(self): + if self.__exc_info is not None: + raise_exc_info(self.__exc_info) + else: + return super(TracebackFuture, self).result() + + class DummyExecutor(object): def submit(self, fn, *args, **kwargs): - future = Future() + future = TracebackFuture() try: future.set_result(fn(*args, **kwargs)) - except Exception as e: - future.set_exception(e) + except Exception: + future.set_exc_info(sys.exc_info()) return future dummy_executor = DummyExecutor() def run_on_executor(fn): + """Decorator to run a synchronous method asynchronously on an executor. + + The decorated method may be called with a ``callback`` keyword + argument and returns a future. + """ @functools.wraps(fn) def wrapper(self, *args, **kwargs): callback = kwargs.pop("callback", None) future = self.executor.submit(fn, self, *args, **kwargs) if callback: - self.io_loop.add_future(future, callback) + self.io_loop.add_future(future, + lambda future: callback(future.result())) return future return wrapper +_NO_RESULT = object() + + def return_future(f): - """Decorator to make a function that returns via callback return a `Future`. + """Decorator to make a function that returns via callback return a + `Future`. The wrapped function should take a ``callback`` keyword argument and invoke it with one argument when it has finished. To signal failure, the function can simply raise an exception (which will be - captured by the `stack_context` and passed along to the `Future`). + captured by the `.StackContext` and passed along to the ``Future``). From the caller's perspective, the callback argument is optional. If one is given, it will be invoked when the function is complete - with the `Future` as an argument. If no callback is given, the caller - should use the `Future` to wait for the function to complete - (perhaps by yielding it in a `gen.engine` function, or passing it - to `IOLoop.add_future`). + with `Future.result()` as an argument. If the function fails, the + callback will not be run and an exception will be raised into the + surrounding `.StackContext`. + + If no callback is given, the caller should use the ``Future`` to + wait for the function to complete (perhaps by yielding it in a + `.gen.engine` function, or passing it to `.IOLoop.add_future`). Usage:: + @return_future def future_func(arg1, arg2, callback): # Do stuff (possibly asynchronous) @@ -142,19 +195,20 @@ def return_future(f): callback() Note that ``@return_future`` and ``@gen.engine`` can be applied to the - same function, provided ``@return_future`` appears first. + same function, provided ``@return_future`` appears first. However, + consider using ``@gen.coroutine`` instead of this combination. """ replacer = ArgReplacer(f, 'callback') + @functools.wraps(f) def wrapper(*args, **kwargs): - future = Future() - callback, args, kwargs = replacer.replace(future.set_result, - args, kwargs) - if callback is not None: - future.add_done_callback(callback) + future = TracebackFuture() + callback, args, kwargs = replacer.replace( + lambda value=_NO_RESULT: future.set_result(value), + args, kwargs) def handle_error(typ, value, tb): - future.set_exception(value) + future.set_exc_info((typ, value, tb)) return True exc_info = None with ExceptionStackContext(handle_error): @@ -172,9 +226,25 @@ def return_future(f): # go ahead and raise it to the caller directly without waiting # for them to inspect the Future. raise_exc_info(exc_info) + + # If the caller passed in a callback, schedule it to be called + # when the future resolves. It is important that this happens + # just before we return the future, or else we risk confusing + # stack contexts with multiple exceptions (one here with the + # immediate exception, and again when the future resolves and + # the callback triggers its exception by calling future.result()). + if callback is not None: + def run_callback(future): + result = future.result() + if result is _NO_RESULT: + callback() + else: + callback(future.result()) + future.add_done_callback(wrap(run_callback)) return future return wrapper + def chain_future(a, b): """Chain two futures together so that when one completes, so does the other. @@ -182,7 +252,10 @@ def chain_future(a, b): """ def copy(future): assert future is a - if a.exception() is not None: + if (isinstance(a, TracebackFuture) and isinstance(b, TracebackFuture) + and a.exc_info() is not None): + b.set_exc_info(a.exc_info()) + elif a.exception() is not None: b.set_exception(a.exception()) else: b.set_result(a.result()) diff --git a/libs/tornado/curl_httpclient.py b/libs/tornado/curl_httpclient.py index f46ea7b8..adc2314f 100755 --- a/libs/tornado/curl_httpclient.py +++ b/libs/tornado/curl_httpclient.py @@ -14,7 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Blocking and non-blocking HTTP client implementations using pycurl.""" +"""Non-blocking HTTP client implementation using pycurl.""" from __future__ import absolute_import, division, print_function, with_statement @@ -30,7 +30,8 @@ from tornado.log import gen_log from tornado import stack_context from tornado.escape import utf8, native_str -from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy +from tornado.httpclient import HTTPResponse, HTTPError, AsyncHTTPClient, main +from tornado.util import bytes_type try: from io import BytesIO # py3 @@ -171,7 +172,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): # libcurl is ready. After each timeout, resync the scheduled # timeout with libcurl's current state. new_timeout = self._multi.timeout() - if new_timeout != -1: + if new_timeout >= 0: self._set_timeout(new_timeout) def _handle_force_timeout(self): @@ -319,7 +320,7 @@ def _curl_setup_request(curl, request, buffer, headers): write_function = request.streaming_callback else: write_function = buffer.write - if type(b'') is type(''): # py2 + if bytes_type is str: # py2 curl.setopt(pycurl.WRITEFUNCTION, write_function) else: # py3 # Upstream pycurl doesn't support py3, but ubuntu 12.10 includes @@ -410,7 +411,14 @@ def _curl_setup_request(curl, request, buffer, headers): if request.auth_username is not None: userpwd = "%s:%s" % (request.auth_username, request.auth_password or '') - curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) + + if request.auth_mode is None or request.auth_mode == "basic": + curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) + elif request.auth_mode == "digest": + curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST) + else: + raise ValueError("Unsupported auth_mode %s" % request.auth_mode) + curl.setopt(pycurl.USERPWD, native_str(userpwd)) gen_log.debug("%s %s (username: %r)", request.method, request.url, request.auth_username) diff --git a/libs/tornado/escape.py b/libs/tornado/escape.py index 6d72532d..016fdade 100755 --- a/libs/tornado/escape.py +++ b/libs/tornado/escape.py @@ -28,9 +28,9 @@ import sys from tornado.util import bytes_type, unicode_type, basestring_type, u try: - from urllib.parse import parse_qs # py3 + from urllib.parse import parse_qs as _parse_qs # py3 except ImportError: - from urlparse import parse_qs # Python 2.6+ + from urlparse import parse_qs as _parse_qs # Python 2.6+ try: import htmlentitydefs # py2 @@ -54,7 +54,7 @@ _XHTML_ESCAPE_DICT = {'&': '&', '<': '<', '>': '>', '"': '"'} def xhtml_escape(value): - """Escapes a string so it is valid within XML or XHTML.""" + """Escapes a string so it is valid within HTML or XML.""" return _XHTML_ESCAPE_RE.sub(lambda match: _XHTML_ESCAPE_DICT[match.group(0)], to_basestring(value)) @@ -72,7 +72,7 @@ def json_encode(value): # the javscript. Some json libraries do this escaping by default, # although python's standard library does not, so we do it here. # http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped - return json.dumps(recursive_unicode(value)).replace("`, + which use ``self.finish()`` in place of a callback argument. """ @functools.wraps(func) def wrapper(*args, **kwargs): @@ -116,20 +136,129 @@ def engine(func): if runner is not None: return runner.handle_exception(typ, value, tb) return False - with ExceptionStackContext(handle_exception) as deactivate: - gen = func(*args, **kwargs) - if isinstance(gen, types.GeneratorType): - runner = Runner(gen, deactivate) - runner.run() - return - assert gen is None, gen - deactivate() + with ExceptionStackContext(handle_exception): + try: + result = func(*args, **kwargs) + except (Return, StopIteration) as e: + result = getattr(e, 'value', None) + else: + if isinstance(result, types.GeneratorType): + def final_callback(value): + if value is not None: + raise ReturnValueIgnoredError( + "@gen.engine functions cannot return values: " + "%r" % (value,)) + assert value is None + runner = Runner(result, final_callback) + runner.run() + return + if result is not None: + raise ReturnValueIgnoredError( + "@gen.engine functions cannot return values: %r" % + (result,)) # no yield, so we're done return wrapper +def coroutine(func): + """Decorator for asynchronous generators. + + Any generator that yields objects from this module must be wrapped + in either this decorator or `engine`. These decorators only work + on functions that are already asynchronous. For + `~tornado.web.RequestHandler` :ref:`HTTP verb methods ` methods, this + means that both the `tornado.web.asynchronous` and + `tornado.gen.coroutine` decorators must be used (for proper + exception handling, ``asynchronous`` should come before + ``gen.coroutine``). + + Coroutines may "return" by raising the special exception + `Return(value) `. In Python 3.3+, it is also possible for + the function to simply use the ``return value`` statement (prior to + Python 3.3 generators were not allowed to also return values). + In all versions of Python a coroutine that simply wishes to exit + early may use the ``return`` statement without a value. + + Functions with this decorator return a `.Future`. Additionally, + they may be called with a ``callback`` keyword argument, which + will be invoked with the future's result when it resolves. If the + coroutine fails, the callback will not be run and an exception + will be raised into the surrounding `.StackContext`. The + ``callback`` argument is not visible inside the decorated + function; it is handled by the decorator itself. + + From the caller's perspective, ``@gen.coroutine`` is similar to + the combination of ``@return_future`` and ``@gen.engine``. + """ + @functools.wraps(func) + def wrapper(*args, **kwargs): + runner = None + future = TracebackFuture() + + if 'callback' in kwargs: + callback = kwargs.pop('callback') + IOLoop.current().add_future( + future, lambda future: callback(future.result())) + + def handle_exception(typ, value, tb): + try: + if runner is not None and runner.handle_exception(typ, value, tb): + return True + except Exception: + typ, value, tb = sys.exc_info() + future.set_exc_info((typ, value, tb)) + return True + with ExceptionStackContext(handle_exception): + try: + result = func(*args, **kwargs) + except (Return, StopIteration) as e: + result = getattr(e, 'value', None) + except Exception: + future.set_exc_info(sys.exc_info()) + return future + else: + if isinstance(result, types.GeneratorType): + def final_callback(value): + future.set_result(value) + runner = Runner(result, final_callback) + runner.run() + return future + future.set_result(result) + return future + return wrapper + + +class Return(Exception): + """Special exception to return a value from a `coroutine`. + + If this exception is raised, its value argument is used as the + result of the coroutine:: + + @gen.coroutine + def fetch_json(url): + response = yield AsyncHTTPClient().fetch(url) + raise gen.Return(json_decode(response.body)) + + In Python 3.3, this exception is no longer necessary: the ``return`` + statement can be used directly to return a value (previously + ``yield`` and ``return`` with a value could not be combined in the + same function). + + By analogy with the return statement, the value argument is optional, + but it is never necessary to ``raise gen.Return()``. The ``return`` + statement can be used with no arguments instead. + """ + def __init__(self, value=None): + super(Return, self).__init__() + self.value = value + + class YieldPoint(object): - """Base class for objects that may be yielded from the generator.""" + """Base class for objects that may be yielded from the generator. + + Applications do not normally need to use this class, but it may be + subclassed to provide additional yielding behavior. + """ def start(self, runner): """Called by the runner after the generator has yielded. @@ -195,7 +324,7 @@ class Wait(YieldPoint): class WaitAll(YieldPoint): - """Returns the results of multiple previous `Callbacks`. + """Returns the results of multiple previous `Callbacks `. The argument is a sequence of `Callback` keys, and the result is a list of results in the same order. @@ -291,7 +420,7 @@ class Multi(YieldPoint): def is_ready(self): finished = list(itertools.takewhile( - lambda i: i.is_ready(), self.unfinished_children)) + lambda i: i.is_ready(), self.unfinished_children)) self.unfinished_children.difference_update(finished) return not self.unfinished_children @@ -331,13 +460,13 @@ class Runner(object): def register_callback(self, key): """Adds ``key`` to the list of callbacks.""" if key in self.pending_callbacks: - raise KeyReuseError("key %r is already pending" % key) + raise KeyReuseError("key %r is already pending" % (key,)) self.pending_callbacks.add(key) def is_ready(self, key): """Returns true if a result is available for ``key``.""" if key not in self.pending_callbacks: - raise UnknownKeyError("key %r is not pending" % key) + raise UnknownKeyError("key %r is not pending" % (key,)) return key in self.results def set_result(self, key, result): @@ -374,7 +503,7 @@ class Runner(object): yielded = self.gen.throw(*exc_info) else: yielded = self.gen.send(next) - except StopIteration: + except (StopIteration, Return) as e: self.finished = True if self.pending_callbacks and not self.had_exception: # If we ran cleanly without waiting on all callbacks @@ -384,7 +513,7 @@ class Runner(object): raise LeakedCallbackError( "finished without waiting for callbacks %r" % self.pending_callbacks) - self.final_callback() + self.final_callback(getattr(e, 'value', None)) self.final_callback = None return except Exception: @@ -401,7 +530,8 @@ class Runner(object): except Exception: self.exc_info = sys.exc_info() else: - self.exc_info = (BadYieldError("yielded unknown object %r" % yielded),) + self.exc_info = (BadYieldError( + "yielded unknown object %r" % (yielded,)),) finally: self.running = False @@ -414,7 +544,7 @@ class Runner(object): else: result = None self.set_result(key, result) - return inner + return wrap(inner) def handle_exception(self, typ, value, tb): if not self.running and not self.finished: diff --git a/libs/tornado/httpclient.py b/libs/tornado/httpclient.py index 1b645504..551fd0b1 100755 --- a/libs/tornado/httpclient.py +++ b/libs/tornado/httpclient.py @@ -1,36 +1,35 @@ """Blocking and non-blocking HTTP client interfaces. This module defines a common interface shared by two implementations, -`simple_httpclient` and `curl_httpclient`. Applications may either +``simple_httpclient`` and ``curl_httpclient``. Applications may either instantiate their chosen implementation class directly or use the `AsyncHTTPClient` class from this module, which selects an implementation that can be overridden with the `AsyncHTTPClient.configure` method. -The default implementation is `simple_httpclient`, and this is expected +The default implementation is ``simple_httpclient``, and this is expected to be suitable for most users' needs. However, some applications may wish -to switch to `curl_httpclient` for reasons such as the following: +to switch to ``curl_httpclient`` for reasons such as the following: -* `curl_httpclient` has some features not found in `simple_httpclient`, +* ``curl_httpclient`` has some features not found in ``simple_httpclient``, including support for HTTP proxies and the ability to use a specified network interface. -* `curl_httpclient` is more likely to be compatible with sites that are +* ``curl_httpclient`` is more likely to be compatible with sites that are not-quite-compliant with the HTTP spec, or sites that use little-exercised features of HTTP. -* `simple_httpclient` only supports SSL on Python 2.6 and above. +* ``curl_httpclient`` is faster. -* `curl_httpclient` is faster +* ``curl_httpclient`` was the default prior to Tornado 2.0. -* `curl_httpclient` was the default prior to Tornado 2.0. - -Note that if you are using `curl_httpclient`, it is highly recommended that +Note that if you are using ``curl_httpclient``, it is highly recommended that you use a recent version of ``libcurl`` and ``pycurl``. Currently the minimum supported version is 7.18.2, and the recommended version is 7.21.1 or newer. """ from __future__ import absolute_import, division, print_function, with_statement +import functools import time import weakref @@ -52,15 +51,15 @@ class HTTPClient(object): try: response = http_client.fetch("http://www.google.com/") print response.body - except httpclient.HTTPError, e: + except httpclient.HTTPError as e: print "Error:", e + httpclient.close() """ def __init__(self, async_client_class=None, **kwargs): self._io_loop = IOLoop() if async_client_class is None: async_client_class = AsyncHTTPClient self._async_client = async_client_class(self._io_loop, **kwargs) - self._response = None self._closed = False def __del__(self): @@ -82,13 +81,8 @@ class HTTPClient(object): If an error occurs during the fetch, we raise an `HTTPError`. """ - def callback(response): - self._response = response - self._io_loop.stop() - self._async_client.fetch(request, callback, **kwargs) - self._io_loop.start() - response = self._response - self._response = None + response = self._io_loop.run_sync(functools.partial( + self._async_client.fetch, request, **kwargs)) response.rethrow() return response @@ -98,26 +92,23 @@ class AsyncHTTPClient(Configurable): Example usage:: - import ioloop - def handle_request(response): if response.error: print "Error:", response.error else: print response.body - ioloop.IOLoop.instance().stop() - http_client = httpclient.AsyncHTTPClient() + http_client = AsyncHTTPClient() http_client.fetch("http://www.google.com/", handle_request) - ioloop.IOLoop.instance().start() - The constructor for this class is magic in several respects: It actually - creates an instance of an implementation-specific subclass, and instances - are reused as a kind of pseudo-singleton (one per IOLoop). The keyword - argument force_instance=True can be used to suppress this singleton - behavior. Constructor arguments other than io_loop and force_instance - are deprecated. The implementation subclass as well as arguments to - its constructor can be set with the static method configure() + The constructor for this class is magic in several respects: It + actually creates an instance of an implementation-specific + subclass, and instances are reused as a kind of pseudo-singleton + (one per `.IOLoop`). The keyword argument ``force_instance=True`` + can be used to suppress this singleton behavior. Constructor + arguments other than ``io_loop`` and ``force_instance`` are + deprecated. The implementation subclass as well as arguments to + its constructor can be set with the static method `configure()` """ @classmethod def configurable_base(cls): @@ -136,7 +127,7 @@ class AsyncHTTPClient(Configurable): return getattr(cls, attr_name) def __new__(cls, io_loop=None, force_instance=False, **kwargs): - io_loop = io_loop or IOLoop.instance() + io_loop = io_loop or IOLoop.current() if io_loop in cls._async_clients() and not force_instance: return cls._async_clients()[io_loop] instance = super(AsyncHTTPClient, cls).__new__(cls, io_loop=io_loop, @@ -152,25 +143,29 @@ class AsyncHTTPClient(Configurable): self.defaults.update(defaults) def close(self): - """Destroys this http client, freeing any file descriptors used. + """Destroys this HTTP client, freeing any file descriptors used. Not needed in normal use, but may be helpful in unittests that create and destroy http clients. No other methods may be called - on the AsyncHTTPClient after close(). + on the `AsyncHTTPClient` after ``close()``. """ if self._async_clients().get(self.io_loop) is self: del self._async_clients()[self.io_loop] def fetch(self, request, callback=None, **kwargs): - """Executes a request, calling callback with an `HTTPResponse`. + """Executes a request, asynchronously returning an `HTTPResponse`. The request may be either a string URL or an `HTTPRequest` object. If it is a string, we construct an `HTTPRequest` using any additional kwargs: ``HTTPRequest(request, **kwargs)`` - If an error occurs during the fetch, the HTTPResponse given to the - callback has a non-None error attribute that contains the exception - encountered during the request. You can call response.rethrow() to - throw the exception (if any) in the callback. + This method returns a `.Future` whose result is an + `HTTPResponse`. The ``Future`` wil raise an `HTTPError` if + the request returned a non-200 response code. + + If a ``callback`` is given, it will be invoked with the `HTTPResponse`. + In the callback interface, `HTTPError` is not automatically raised. + Instead, you must check the response's ``error`` attribute or + call its `~HTTPResponse.rethrow` method. """ if not isinstance(request, HTTPRequest): request = HTTPRequest(url=request, **kwargs) @@ -182,6 +177,7 @@ class AsyncHTTPClient(Configurable): future = Future() if callback is not None: callback = stack_context.wrap(callback) + def handle_future(future): exc = future.exception() if isinstance(exc, HTTPError) and exc.response is not None: @@ -194,6 +190,7 @@ class AsyncHTTPClient(Configurable): response = future.result() self.io_loop.add_callback(callback, response) future.add_done_callback(handle_future) + def handle_response(response): if response.error: future.set_exception(response.error) @@ -207,19 +204,19 @@ class AsyncHTTPClient(Configurable): @classmethod def configure(cls, impl, **kwargs): - """Configures the AsyncHTTPClient subclass to use. + """Configures the `AsyncHTTPClient` subclass to use. - AsyncHTTPClient() actually creates an instance of a subclass. + ``AsyncHTTPClient()`` actually creates an instance of a subclass. This method may be called with either a class object or the - fully-qualified name of such a class (or None to use the default, - SimpleAsyncHTTPClient) + fully-qualified name of such a class (or ``None`` to use the default, + ``SimpleAsyncHTTPClient``) If additional keyword arguments are given, they will be passed to the constructor of each subclass instance created. The - keyword argument max_clients determines the maximum number of - simultaneous fetch() operations that can execute in parallel - on each IOLoop. Additional arguments may be supported depending - on the implementation class in use. + keyword argument ``max_clients`` determines the maximum number + of simultaneous `~AsyncHTTPClient.fetch()` operations that can + execute in parallel on each `.IOLoop`. Additional arguments + may be supported depending on the implementation class in use. Example:: @@ -245,7 +242,7 @@ class HTTPRequest(object): validate_cert=True) def __init__(self, url, method="GET", headers=None, body=None, - auth_username=None, auth_password=None, + auth_username=None, auth_password=None, auth_mode=None, connect_timeout=None, request_timeout=None, if_modified_since=None, follow_redirects=None, max_redirects=None, user_agent=None, use_gzip=None, @@ -256,59 +253,61 @@ class HTTPRequest(object): validate_cert=None, ca_certs=None, allow_ipv6=None, client_key=None, client_cert=None): - r"""Creates an `HTTPRequest`. - - All parameters except `url` are optional. + r"""All parameters except ``url`` are optional. :arg string url: URL to fetch :arg string method: HTTP method, e.g. "GET" or "POST" :arg headers: Additional HTTP headers to pass on the request :type headers: `~tornado.httputil.HTTPHeaders` or `dict` - :arg string auth_username: Username for HTTP "Basic" authentication - :arg string auth_password: Password for HTTP "Basic" authentication + :arg string auth_username: Username for HTTP authentication + :arg string auth_password: Password for HTTP authentication + :arg string auth_mode: Authentication mode; default is "basic". + Allowed values are implementation-defined; ``curl_httpclient`` + supports "basic" and "digest"; ``simple_httpclient`` only supports + "basic" :arg float connect_timeout: Timeout for initial connection in seconds :arg float request_timeout: Timeout for entire request in seconds - :arg datetime if_modified_since: Timestamp for ``If-Modified-Since`` - header + :arg if_modified_since: Timestamp for ``If-Modified-Since`` header + :type if_modified_since: `datetime` or `float` :arg bool follow_redirects: Should redirects be followed automatically or return the 3xx response? - :arg int max_redirects: Limit for `follow_redirects` + :arg int max_redirects: Limit for ``follow_redirects`` :arg string user_agent: String to send as ``User-Agent`` header :arg bool use_gzip: Request gzip encoding from the server :arg string network_interface: Network interface to use for request - :arg callable streaming_callback: If set, `streaming_callback` will + :arg callable streaming_callback: If set, ``streaming_callback`` will be run with each chunk of data as it is received, and - `~HTTPResponse.body` and `~HTTPResponse.buffer` will be empty in + ``HTTPResponse.body`` and ``HTTPResponse.buffer`` will be empty in the final response. - :arg callable header_callback: If set, `header_callback` will + :arg callable header_callback: If set, ``header_callback`` will be run with each header line as it is received (including the first line, e.g. ``HTTP/1.0 200 OK\r\n``, and a final line containing only ``\r\n``. All lines include the trailing newline - characters). `~HTTPResponse.headers` will be empty in the final + characters). ``HTTPResponse.headers`` will be empty in the final response. This is most useful in conjunction with - `streaming_callback`, because it's the only way to get access to + ``streaming_callback``, because it's the only way to get access to header data while the request is in progress. :arg callable prepare_curl_callback: If set, will be called with - a `pycurl.Curl` object to allow the application to make additional - `setopt` calls. + a ``pycurl.Curl`` object to allow the application to make additional + ``setopt`` calls. :arg string proxy_host: HTTP proxy hostname. To use proxies, - `proxy_host` and `proxy_port` must be set; `proxy_username` and - `proxy_pass` are optional. Proxies are currently only support - with `curl_httpclient`. + ``proxy_host`` and ``proxy_port`` must be set; ``proxy_username`` and + ``proxy_pass`` are optional. Proxies are currently only supported + with ``curl_httpclient``. :arg int proxy_port: HTTP proxy port :arg string proxy_username: HTTP proxy username :arg string proxy_password: HTTP proxy password - :arg bool allow_nonstandard_methods: Allow unknown values for `method` + :arg bool allow_nonstandard_methods: Allow unknown values for ``method`` argument? :arg bool validate_cert: For HTTPS requests, validate the server's certificate? :arg string ca_certs: filename of CA certificates in PEM format, - or None to use defaults. Note that in `curl_httpclient`, if - any request uses a custom `ca_certs` file, they all must (they - don't have to all use the same `ca_certs`, but it's not possible - to mix requests with ca_certs and requests that use the defaults. + or None to use defaults. Note that in ``curl_httpclient``, if + any request uses a custom ``ca_certs`` file, they all must (they + don't have to all use the same ``ca_certs``, but it's not possible + to mix requests with ``ca_certs`` and requests that use the defaults. :arg bool allow_ipv6: Use IPv6 when available? Default is false in - `simple_httpclient` and true in `curl_httpclient` + ``simple_httpclient`` and true in ``curl_httpclient`` :arg string client_key: Filename for client SSL key, if any :arg string client_cert: Filename for client SSL certificate, if any """ @@ -327,6 +326,7 @@ class HTTPRequest(object): self.body = utf8(body) self.auth_username = auth_username self.auth_password = auth_password + self.auth_mode = auth_mode self.connect_timeout = connect_timeout self.request_timeout = request_timeout self.follow_redirects = follow_redirects @@ -356,29 +356,32 @@ class HTTPResponse(object): * code: numeric HTTP status code, e.g. 200 or 404 * reason: human-readable reason phrase describing the status code - (with curl_httpclient, this is a default value rather than the - server's actual response) + (with curl_httpclient, this is a default value rather than the + server's actual response) - * headers: httputil.HTTPHeaders object + * headers: `tornado.httputil.HTTPHeaders` object - * buffer: cStringIO object for response body + * buffer: ``cStringIO`` object for response body - * body: response body as string (created on demand from self.buffer) + * body: response body as string (created on demand from ``self.buffer``) * error: Exception object, if any * request_time: seconds from request start to finish * time_info: dictionary of diagnostic timing information from the request. - Available data are subject to change, but currently uses timings - available from http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html, - plus 'queue', which is the delay (if any) introduced by waiting for - a slot under AsyncHTTPClient's max_clients setting. + Available data are subject to change, but currently uses timings + available from http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html, + plus ``queue``, which is the delay (if any) introduced by waiting for + a slot under `AsyncHTTPClient`'s ``max_clients`` setting. """ def __init__(self, request, code, headers=None, buffer=None, effective_url=None, error=None, request_time=None, time_info=None, reason=None): - self.request = request + if isinstance(request, _RequestProxy): + self.request = request.request + else: + self.request = request self.code = code self.reason = reason or httputil.responses.get(code, "Unknown") if headers is not None: @@ -426,13 +429,13 @@ class HTTPError(Exception): Attributes: - code - HTTP error integer error code, e.g. 404. Error code 599 is - used when no HTTP response was received, e.g. for a timeout. + * ``code`` - HTTP error integer error code, e.g. 404. Error code 599 is + used when no HTTP response was received, e.g. for a timeout. - response - HTTPResponse object, if any. + * ``response`` - `HTTPResponse` object, if any. - Note that if follow_redirects is False, redirects become HTTPErrors, - and you can look at error.response.headers['Location'] to see the + Note that if ``follow_redirects`` is False, redirects become HTTPErrors, + and you can look at ``error.response.headers['Location']`` to see the destination of the redirect. """ def __init__(self, code, message=None, response=None): diff --git a/libs/tornado/httpserver.py b/libs/tornado/httpserver.py index 24d5e6b3..ef36e6bd 100755 --- a/libs/tornado/httpserver.py +++ b/libs/tornado/httpserver.py @@ -34,14 +34,15 @@ from tornado.escape import native_str, parse_qs_bytes from tornado import httputil from tornado import iostream from tornado.log import gen_log +from tornado import netutil from tornado.tcpserver import TCPServer from tornado import stack_context from tornado.util import bytes_type try: - import Cookie # py2 + import Cookie # py2 except ImportError: - import http.cookies as Cookie # py3 + import http.cookies as Cookie # py3 class HTTPServer(TCPServer): @@ -54,8 +55,8 @@ class HTTPServer(TCPServer): requests). A simple example server that echoes back the URI you requested:: - import httpserver - import ioloop + import tornado.httpserver + import tornado.ioloop def handle_request(request): message = "You requested %s\n" % request.uri @@ -63,9 +64,9 @@ class HTTPServer(TCPServer): len(message), message)) request.finish() - http_server = httpserver.HTTPServer(handle_request) + http_server = tornado.httpserver.HTTPServer(handle_request) http_server.listen(8888) - ioloop.IOLoop.instance().start() + tornado.ioloop.IOLoop.instance().start() `HTTPServer` is a very basic connection handler. It parses the request headers and body, but the request callback is responsible for producing @@ -92,11 +93,10 @@ class HTTPServer(TCPServer): if Tornado is run behind an SSL-decoding proxy that does not set one of the supported ``xheaders``. - `HTTPServer` can serve SSL traffic with Python 2.6+ and OpenSSL. - To make this server serve SSL traffic, send the ssl_options dictionary + To make this server serve SSL traffic, send the ``ssl_options`` dictionary argument with the arguments required for the `ssl.wrap_socket` method, - including "certfile" and "keyfile". In Python 3.2+ you can pass - an `ssl.SSLContext` object instead of a dict:: + including ``certfile`` and ``keyfile``. (In Python 3.2+ you can pass + an `ssl.SSLContext` object instead of a dict):: HTTPServer(applicaton, ssl_options={ "certfile": os.path.join(data_dir, "mydomain.crt"), @@ -123,9 +123,9 @@ class HTTPServer(TCPServer): server.start(0) # Forks multiple sub-processes IOLoop.instance().start() - When using this interface, an `IOLoop` must *not* be passed - to the `HTTPServer` constructor. `start` will always start - the server on the default singleton `IOLoop`. + When using this interface, an `.IOLoop` must *not* be passed + to the `HTTPServer` constructor. `~.TCPServer.start` will always start + the server on the default singleton `.IOLoop`. 3. `~tornado.tcpserver.TCPServer.add_sockets`: advanced multi-process:: @@ -135,21 +135,21 @@ class HTTPServer(TCPServer): server.add_sockets(sockets) IOLoop.instance().start() - The `add_sockets` interface is more complicated, but it can be - used with `tornado.process.fork_processes` to give you more - flexibility in when the fork happens. `add_sockets` can - also be used in single-process servers if you want to create - your listening sockets in some way other than - `tornado.netutil.bind_sockets`. + The `~.TCPServer.add_sockets` interface is more complicated, + but it can be used with `tornado.process.fork_processes` to + give you more flexibility in when the fork happens. + `~.TCPServer.add_sockets` can also be used in single-process + servers if you want to create your listening sockets in some + way other than `tornado.netutil.bind_sockets`. """ - def __init__(self, request_callback, no_keep_alive=False, io_loop=None, - xheaders=False, ssl_options=None, protocol=None, **kwargs): + def __init__(self, request_callback, no_keep_alive = False, io_loop = None, + xheaders = False, ssl_options = None, protocol = None, **kwargs): self.request_callback = request_callback self.no_keep_alive = no_keep_alive self.xheaders = xheaders self.protocol = protocol - TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options, + TCPServer.__init__(self, io_loop = io_loop, ssl_options = ssl_options, **kwargs) def handle_stream(self, stream, address): @@ -168,8 +168,8 @@ class HTTPConnection(object): We parse HTTP headers and bodies, and execute the request callback until the HTTP conection is closed. """ - def __init__(self, stream, address, request_callback, no_keep_alive=False, - xheaders=False, protocol=None): + def __init__(self, stream, address, request_callback, no_keep_alive = False, + xheaders = False, protocol = None): self.stream = stream self.address = address # Save the socket's address family now so we know how to @@ -182,31 +182,51 @@ class HTTPConnection(object): self.protocol = protocol self._request = None self._request_finished = False + self._write_callback = None + self._close_callback = None # Save stack context here, outside of any request. This keeps # contexts from one request from leaking into the next. self._header_callback = stack_context.wrap(self._on_headers) self.stream.read_until(b"\r\n\r\n", self._header_callback) + + def _clear_callbacks(self): + """Clears the per-request callbacks. + + This is run in between requests to allow the previous handler + to be garbage collected (and prevent spurious close callbacks), + and when the connection is closed (to break up cycles and + facilitate garbage collection in cpython). + """ self._write_callback = None self._close_callback = None def set_close_callback(self, callback): + """Sets a callback that will be run when the connection is closed. + + Use this instead of accessing + `HTTPConnection.stream.set_close_callback + <.BaseIOStream.set_close_callback>` directly (which was the + recommended approach prior to Tornado 3.0). + """ self._close_callback = stack_context.wrap(callback) self.stream.set_close_callback(self._on_connection_close) def _on_connection_close(self): callback = self._close_callback self._close_callback = None - callback() + if callback: callback() # Delete any unfinished callbacks to break up reference cycles. - self._write_callback = None + self._header_callback = None + self._clear_callbacks() def close(self): self.stream.close() # Remove this reference to self, which would otherwise cause a # cycle and delay garbage collection of this connection. self._header_callback = None + self._clear_callbacks() - def write(self, chunk, callback=None): + def write(self, chunk, callback = None): """Writes a chunk of output to the stream.""" assert self._request, "Request closed" if not self.stream.closed(): @@ -251,6 +271,7 @@ class HTTPConnection(object): disconnect = True self._request = None self._request_finished = False + self._clear_callbacks() if disconnect: self.close() return @@ -273,7 +294,11 @@ class HTTPConnection(object): raise _BadRequestException("Malformed HTTP request line") if not version.startswith("HTTP/"): raise _BadRequestException("Malformed HTTP version in HTTP Request-Line") - headers = httputil.HTTPHeaders.parse(data[eol:]) + try: + headers = httputil.HTTPHeaders.parse(data[eol:]) + except ValueError: + # Probably from split() if there was no ':' in the line + raise _BadRequestException("Malformed HTTP headers") # HTTPRequest wants an IP, not a full socket address if self.address_family in (socket.AF_INET, socket.AF_INET6): @@ -283,8 +308,8 @@ class HTTPConnection(object): remote_ip = '0.0.0.0' self._request = HTTPRequest( - connection=self, method=method, uri=uri, version=version, - headers=headers, remote_ip=remote_ip, protocol=self.protocol) + connection = self, method = method, uri = uri, version = version, + headers = headers, remote_ip = remote_ip, protocol = self.protocol) content_length = headers.get("Content-Length") if content_length: @@ -339,7 +364,7 @@ class HTTPRequest(object): .. attribute:: headers - `HTTPHeader` dictionary-like object for request headers. Acts like + `.HTTPHeaders` dictionary-like object for request headers. Acts like a case-insensitive dictionary with additional methods for repeated headers. @@ -349,13 +374,13 @@ class HTTPRequest(object): .. attribute:: remote_ip - Client's IP address as a string. If `HTTPServer.xheaders` is set, + Client's IP address as a string. If ``HTTPServer.xheaders`` is set, will pass along the real IP address provided by a load balancer in the ``X-Real-Ip`` header .. attribute:: protocol - The protocol used, either "http" or "https". If `HTTPServer.xheaders` + The protocol used, either "http" or "https". If ``HTTPServer.xheaders`` is set, will pass along the protocol used by a load balancer if reported via an ``X-Scheme`` header. @@ -369,13 +394,13 @@ class HTTPRequest(object): maps arguments names to lists of values (to support multiple values for individual names). Names are of type `str`, while arguments are byte strings. Note that this is different from - `RequestHandler.get_argument`, which returns argument values as + `.RequestHandler.get_argument`, which returns argument values as unicode strings. .. attribute:: files File uploads are available in the files property, which maps file - names to lists of :class:`HTTPFile`. + names to lists of `.HTTPFile`. .. attribute:: connection @@ -384,34 +409,39 @@ class HTTPRequest(object): are typically kept open in HTTP/1.1, multiple requests can be handled sequentially on a single connection. """ - def __init__(self, method, uri, version="HTTP/1.0", headers=None, - body=None, remote_ip=None, protocol=None, host=None, - files=None, connection=None): + def __init__(self, method, uri, version = "HTTP/1.0", headers = None, + body = None, remote_ip = None, protocol = None, host = None, + files = None, connection = None): self.method = method self.uri = uri self.version = version self.headers = headers or httputil.HTTPHeaders() self.body = body or "" + + # set remote IP and protocol + self.remote_ip = remote_ip + if protocol: + self.protocol = protocol + elif connection and isinstance(connection.stream, + iostream.SSLIOStream): + self.protocol = "https" + else: + self.protocol = "http" + + # xheaders can override the defaults if connection and connection.xheaders: # Squid uses X-Forwarded-For, others use X-Real-Ip - self.remote_ip = self.headers.get( - "X-Real-Ip", self.headers.get("X-Forwarded-For", remote_ip)) - if not self._valid_ip(self.remote_ip): - self.remote_ip = remote_ip + ip = self.headers.get( + "X-Real-Ip", self.headers.get("X-Forwarded-For", self.remote_ip)) + if netutil.is_valid_ip(ip): + self.remote_ip = ip # AWS uses X-Forwarded-Proto - self.protocol = self.headers.get( - "X-Scheme", self.headers.get("X-Forwarded-Proto", protocol)) - if self.protocol not in ("http", "https"): - self.protocol = "http" - else: - self.remote_ip = remote_ip - if protocol: - self.protocol = protocol - elif connection and isinstance(connection.stream, - iostream.SSLIOStream): - self.protocol = "https" - else: - self.protocol = "http" + proto = self.headers.get( + "X-Scheme", self.headers.get("X-Forwarded-Proto", self.protocol)) + if proto in ("http", "https"): + self.protocol = proto + + self.host = host or self.headers.get("Host") or "127.0.0.1" self.files = files or {} self.connection = connection @@ -419,7 +449,7 @@ class HTTPRequest(object): self._finish_time = None self.path, sep, self.query = uri.partition('?') - self.arguments = parse_qs_bytes(self.query, keep_blank_values=True) + self.arguments = parse_qs_bytes(self.query, keep_blank_values = True) def supports_http_1_1(self): """Returns True if this request supports HTTP/1.1 semantics""" @@ -438,10 +468,10 @@ class HTTPRequest(object): self._cookies = {} return self._cookies - def write(self, chunk, callback=None): + def write(self, chunk, callback = None): """Writes the given chunk to the response stream.""" assert isinstance(chunk, bytes_type) - self.connection.write(chunk, callback=callback) + self.connection.write(chunk, callback = callback) def finish(self): """Finishes this HTTP request on the open connection.""" @@ -459,7 +489,7 @@ class HTTPRequest(object): else: return self._finish_time - self._start_time - def get_ssl_certificate(self, binary_form=False): + def get_ssl_certificate(self, binary_form = False): """Returns the client's SSL certificate, if any. To use client certificates, the HTTPServer must have been constructed @@ -481,7 +511,7 @@ class HTTPRequest(object): """ try: return self.connection.stream.socket.getpeercert( - binary_form=binary_form) + binary_form = binary_form) except ssl.SSLError: return None @@ -491,15 +521,3 @@ class HTTPRequest(object): args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs]) return "%s(%s, headers=%s)" % ( self.__class__.__name__, args, dict(self.headers)) - - def _valid_ip(self, ip): - try: - res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC, - socket.SOCK_STREAM, - 0, socket.AI_NUMERICHOST) - return bool(res) - except socket.gaierror as e: - if e.args[0] == socket.EAI_NONAME: - return False - raise - return True diff --git a/libs/tornado/httputil.py b/libs/tornado/httputil.py index 94b8ba4f..a09aeabf 100755 --- a/libs/tornado/httputil.py +++ b/libs/tornado/httputil.py @@ -32,6 +32,10 @@ try: except ImportError: from http.client import responses # py3 +# responses is unused in this file, but we re-export it to other files. +# Reference it so pyflakes doesn't complain. +responses + try: from urllib import urlencode # py2 except ImportError: @@ -39,11 +43,12 @@ except ImportError: class HTTPHeaders(dict): - """A dictionary that maintains Http-Header-Case for all keys. + """A dictionary that maintains ``Http-Header-Case`` for all keys. Supports multiple values per key via a pair of new methods, - add() and get_list(). The regular dictionary interface returns a single - value per key, with multiple values joined by a comma. + `add()` and `get_list()`. The regular dictionary interface + returns a single value per key, with multiple values joined by a + comma. >>> h = HTTPHeaders({"content-type": "text/html"}) >>> list(h.keys()) @@ -209,13 +214,14 @@ def url_concat(url, args): class HTTPFile(ObjectDict): - """Represents an HTTP file. For backwards compatibility, its instance - attributes are also accessible as dictionary keys. + """Represents a file uploaded via a form. - :ivar filename: - :ivar body: - :ivar content_type: The content_type comes from the provided HTTP header - and should not be trusted outright given that it can be easily forged. + For backwards compatibility, its instance attributes are also + accessible as dictionary keys. + + * ``filename`` + * ``body`` + * ``content_type`` """ pass @@ -223,15 +229,15 @@ class HTTPFile(ObjectDict): def parse_body_arguments(content_type, body, arguments, files): """Parses a form request body. - Supports "application/x-www-form-urlencoded" and "multipart/form-data". - The content_type parameter should be a string and body should be - a byte string. The arguments and files parameters are dictionaries - that will be updated with the parsed contents. + Supports ``application/x-www-form-urlencoded`` and + ``multipart/form-data``. The ``content_type`` parameter should be + a string and ``body`` should be a byte string. The ``arguments`` + and ``files`` parameters are dictionaries that will be updated + with the parsed contents. """ if content_type.startswith("application/x-www-form-urlencoded"): - uri_arguments = parse_qs_bytes(native_str(body)) + uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) for name, values in uri_arguments.items(): - values = [v for v in values if v] if values: arguments.setdefault(name, []).extend(values) elif content_type.startswith("multipart/form-data"): @@ -246,9 +252,9 @@ def parse_body_arguments(content_type, body, arguments, files): def parse_multipart_form_data(boundary, data, arguments, files): - """Parses a multipart/form-data body. + """Parses a ``multipart/form-data`` body. - The boundary and data parameters are both byte strings. + The ``boundary`` and ``data`` parameters are both byte strings. The dictionaries given in the arguments and files parameters will be updated with the contents of the body. """ @@ -294,8 +300,8 @@ def parse_multipart_form_data(boundary, data, arguments, files): def format_timestamp(ts): """Formats a timestamp in the format used by HTTP. - The argument may be a numeric timestamp as returned by `time.time()`, - a time tuple as returned by `time.gmtime()`, or a `datetime.datetime` + The argument may be a numeric timestamp as returned by `time.time`, + a time tuple as returned by `time.gmtime`, or a `datetime.datetime` object. >>> format_timestamp(1359312200) @@ -314,6 +320,8 @@ def format_timestamp(ts): # _parseparam and _parse_header are copied and modified from python2.7's cgi.py # The original 2.7 version of this code did not correctly support some # combinations of semicolons and double quotes. + + def _parseparam(s): while s[:1] == ';': s = s[1:] diff --git a/libs/tornado/ioloop.py b/libs/tornado/ioloop.py index 4062661b..dd9639c0 100755 --- a/libs/tornado/ioloop.py +++ b/libs/tornado/ioloop.py @@ -36,11 +36,12 @@ import logging import numbers import os import select +import sys import threading import time import traceback -from tornado.concurrent import DummyFuture +from tornado.concurrent import Future, TracebackFuture from tornado.log import app_log, gen_log from tornado import stack_context from tornado.util import Configurable @@ -50,11 +51,6 @@ try: except ImportError: signal = None -try: - from concurrent import futures -except ImportError: - futures = None - try: import thread # py2 except ImportError: @@ -63,14 +59,18 @@ except ImportError: from tornado.platform.auto import set_close_exec, Waker +class TimeoutError(Exception): + pass + + class IOLoop(Configurable): """A level-triggered I/O loop. - We use epoll (Linux) or kqueue (BSD and Mac OS X; requires python - 2.6+) if they are available, or else we fall back on select(). If - you are implementing a system that needs to handle thousands of - simultaneous connections, you should use a system that supports either - epoll or kqueue. + We use ``epoll`` (Linux) or ``kqueue`` (BSD and Mac OS X) if they + are available, or else we fall back on select(). If you are + implementing a system that needs to handle thousands of + simultaneous connections, you should use a system that supports + either ``epoll`` or ``kqueue``. Example usage for a simple TCP server:: @@ -125,19 +125,11 @@ class IOLoop(Configurable): @staticmethod def instance(): - """Returns a global IOLoop instance. + """Returns a global `IOLoop` instance. - Most single-threaded applications have a single, global IOLoop. - Use this method instead of passing around IOLoop instances - throughout your code. - - A common pattern for classes that depend on IOLoops is to use - a default argument to enable programs with multiple IOLoops - but not require the argument for simpler applications:: - - class MyClass(object): - def __init__(self, io_loop=None): - self.io_loop = io_loop or IOLoop.instance() + Most applications have a single, global `IOLoop` running on the + main thread. Use this method to get this instance from + another thread. To get the current thread's `IOLoop`, use `current()`. """ if not hasattr(IOLoop, "_instance"): with IOLoop._instance_lock: @@ -152,27 +144,54 @@ class IOLoop(Configurable): return hasattr(IOLoop, "_instance") def install(self): - """Installs this IOloop object as the singleton instance. + """Installs this `IOLoop` object as the singleton instance. This is normally not necessary as `instance()` will create - an IOLoop on demand, but you may want to call `install` to use - a custom subclass of IOLoop. + an `IOLoop` on demand, but you may want to call `install` to use + a custom subclass of `IOLoop`. """ assert not IOLoop.initialized() IOLoop._instance = self @staticmethod def current(): + """Returns the current thread's `IOLoop`. + + If an `IOLoop` is currently running or has been marked as current + by `make_current`, returns that instance. Otherwise returns + `IOLoop.instance()`, i.e. the main thread's `IOLoop`. + + A common pattern for classes that depend on ``IOLoops`` is to use + a default argument to enable programs with multiple ``IOLoops`` + but not require the argument for simpler applications:: + + class MyClass(object): + def __init__(self, io_loop=None): + self.io_loop = io_loop or IOLoop.current() + + In general you should use `IOLoop.current` as the default when + constructing an asynchronous object, and use `IOLoop.instance` + when you mean to communicate to the main thread from a different + one. + """ current = getattr(IOLoop._current, "instance", None) if current is None: - raise ValueError("no current IOLoop") + return IOLoop.instance() return current def make_current(self): + """Makes this the `IOLoop` for the current thread. + + An `IOLoop` automatically becomes current for its thread + when it is started, but it is sometimes useful to call + `make_current` explictly before starting the `IOLoop`, + so that code run at startup time can find the right + instance. + """ IOLoop._current.instance = self - def clear_current(self): - assert IOLoop._current.instance is self + @staticmethod + def clear_current(): IOLoop._current.instance = None @classmethod @@ -195,19 +214,20 @@ class IOLoop(Configurable): pass def close(self, all_fds=False): - """Closes the IOLoop, freeing any resources used. + """Closes the `IOLoop`, freeing any resources used. If ``all_fds`` is true, all file descriptors registered on the - IOLoop will be closed (not just the ones created by the IOLoop itself). + IOLoop will be closed (not just the ones created by the + `IOLoop` itself). - Many applications will only use a single IOLoop that runs for the - entire lifetime of the process. In that case closing the IOLoop + Many applications will only use a single `IOLoop` that runs for the + entire lifetime of the process. In that case closing the `IOLoop` is not necessary since everything will be cleaned up when the process exits. `IOLoop.close` is provided mainly for scenarios such as unit tests, which create and destroy a large number of - IOLoops. + ``IOLoops``. - An IOLoop must be completely stopped before it can be closed. This + An `IOLoop` must be completely stopped before it can be closed. This means that `IOLoop.stop()` must be called *and* `IOLoop.start()` must be allowed to return before attempting to call `IOLoop.close()`. Therefore the call to `close` will usually appear just after @@ -216,7 +236,13 @@ class IOLoop(Configurable): raise NotImplementedError() def add_handler(self, fd, handler, events): - """Registers the given handler to receive the given events for fd.""" + """Registers the given handler to receive the given events for fd. + + The ``events`` argument is a bitwise or of the constants + ``IOLoop.READ``, ``IOLoop.WRITE``, and ``IOLoop.ERROR``. + + When an event occurs, ``handler(fd, events)`` will be run. + """ raise NotImplementedError() def update_handler(self, fd, events): @@ -228,28 +254,32 @@ class IOLoop(Configurable): raise NotImplementedError() def set_blocking_signal_threshold(self, seconds, action): - """Sends a signal if the ioloop is blocked for more than s seconds. + """Sends a signal if the `IOLoop` is blocked for more than + ``s`` seconds. - Pass seconds=None to disable. Requires python 2.6 on a unixy + Pass ``seconds=None`` to disable. Requires Python 2.6 on a unixy platform. - The action parameter is a python signal handler. Read the - documentation for the python 'signal' module for more information. - If action is None, the process will be killed if it is blocked for - too long. + The action parameter is a Python signal handler. Read the + documentation for the `signal` module for more information. + If ``action`` is None, the process will be killed if it is + blocked for too long. """ raise NotImplementedError() def set_blocking_log_threshold(self, seconds): - """Logs a stack trace if the ioloop is blocked for more than s seconds. - Equivalent to set_blocking_signal_threshold(seconds, self.log_stack) + """Logs a stack trace if the `IOLoop` is blocked for more than + ``s`` seconds. + + Equivalent to ``set_blocking_signal_threshold(seconds, + self.log_stack)`` """ self.set_blocking_signal_threshold(seconds, self.log_stack) def log_stack(self, signal, frame): """Signal handler to log the stack trace of the current thread. - For use with set_blocking_signal_threshold. + For use with `set_blocking_signal_threshold`. """ gen_log.warning('IOLoop blocked for %f seconds in\n%s', self._blocking_signal_threshold, @@ -258,7 +288,7 @@ class IOLoop(Configurable): def start(self): """Starts the I/O loop. - The loop will run until one of the I/O handlers calls stop(), which + The loop will run until one of the callbacks calls `stop()`, which will make the loop stop after the current event iteration completes. """ raise NotImplementedError() @@ -266,7 +296,7 @@ class IOLoop(Configurable): def stop(self): """Stop the I/O loop. - If the event loop is not currently running, the next call to start() + If the event loop is not currently running, the next call to `start()` will return immediately. To use asynchronous methods from otherwise-synchronous code (such as @@ -276,23 +306,71 @@ class IOLoop(Configurable): async_method(ioloop=ioloop, callback=ioloop.stop) ioloop.start() - ioloop.start() will return after async_method has run its callback, - whether that callback was invoked before or after ioloop.start. + ``ioloop.start()`` will return after ``async_method`` has run + its callback, whether that callback was invoked before or + after ``ioloop.start``. - Note that even after `stop` has been called, the IOLoop is not + Note that even after `stop` has been called, the `IOLoop` is not completely stopped until `IOLoop.start` has also returned. Some work that was scheduled before the call to `stop` may still - be run before the IOLoop shuts down. + be run before the `IOLoop` shuts down. """ raise NotImplementedError() + def run_sync(self, func, timeout=None): + """Starts the `IOLoop`, runs the given function, and stops the loop. + + If the function returns a `.Future`, the `IOLoop` will run + until the future is resolved. If it raises an exception, the + `IOLoop` will stop and the exception will be re-raised to the + caller. + + The keyword-only argument ``timeout`` may be used to set + a maximum duration for the function. If the timeout expires, + a `TimeoutError` is raised. + + This method is useful in conjunction with `tornado.gen.coroutine` + to allow asynchronous calls in a ``main()`` function:: + + @gen.coroutine + def main(): + # do stuff... + + if __name__ == '__main__': + IOLoop.instance().run_sync(main) + """ + future_cell = [None] + + def run(): + try: + result = func() + except Exception: + future_cell[0] = TracebackFuture() + future_cell[0].set_exc_info(sys.exc_info()) + else: + if isinstance(result, Future): + future_cell[0] = result + else: + future_cell[0] = Future() + future_cell[0].set_result(result) + self.add_future(future_cell[0], lambda future: self.stop()) + self.add_callback(run) + if timeout is not None: + timeout_handle = self.add_timeout(self.time() + timeout, self.stop) + self.start() + if timeout is not None: + self.remove_timeout(timeout_handle) + if not future_cell[0].done(): + raise TimeoutError('Operation timed out after %s seconds' % timeout) + return future_cell[0].result() + def time(self): - """Returns the current time according to the IOLoop's clock. + """Returns the current time according to the `IOLoop`'s clock. The return value is a floating-point number relative to an unspecified time in the past. - By default, the IOLoop's time function is `time.time`. However, + By default, the `IOLoop`'s time function is `time.time`. However, it may be configured to use e.g. `time.monotonic` instead. Calls to `add_timeout` that pass a number instead of a `datetime.timedelta` should use this function to compute the @@ -302,24 +380,26 @@ class IOLoop(Configurable): return time.time() def add_timeout(self, deadline, callback): - """Calls the given callback at the time deadline from the I/O loop. + """Runs the ``callback`` at the time ``deadline`` from the I/O loop. - Returns a handle that may be passed to remove_timeout to cancel. + Returns an opaque handle that may be passed to + `remove_timeout` to cancel. - ``deadline`` may be a number denoting a time relative to - `IOLoop.time`, or a ``datetime.timedelta`` object for a - deadline relative to the current time. + ``deadline`` may be a number denoting a time (on the same + scale as `IOLoop.time`, normally `time.time`), or a + `datetime.timedelta` object for a deadline relative to the + current time. Note that it is not safe to call `add_timeout` from other threads. Instead, you must use `add_callback` to transfer control to the - IOLoop's thread, and then call `add_timeout` from there. + `IOLoop`'s thread, and then call `add_timeout` from there. """ raise NotImplementedError() def remove_timeout(self, timeout): """Cancels a pending timeout. - The argument is a handle as returned by add_timeout. It is + The argument is a handle as returned by `add_timeout`. It is safe to call `remove_timeout` even if the callback has already been run. """ @@ -329,11 +409,11 @@ class IOLoop(Configurable): """Calls the given callback on the next I/O loop iteration. It is safe to call this method from any thread at any time, - except from a signal handler. Note that this is the *only* - method in IOLoop that makes this thread-safety guarantee; all - other interaction with the IOLoop must be done from that - IOLoop's thread. add_callback() may be used to transfer - control from other threads to the IOLoop's thread. + except from a signal handler. Note that this is the **only** + method in `IOLoop` that makes this thread-safety guarantee; all + other interaction with the `IOLoop` must be done from that + `IOLoop`'s thread. `add_callback()` may be used to transfer + control from other threads to the `IOLoop`'s thread. To add a callback from a signal handler, see `add_callback_from_signal`. @@ -347,22 +427,19 @@ class IOLoop(Configurable): otherwise. Callbacks added with this method will be run without any - stack_context, to avoid picking up the context of the function + `.stack_context`, to avoid picking up the context of the function that was interrupted by the signal. """ raise NotImplementedError() - if futures is not None: - _FUTURE_TYPES = (futures.Future, DummyFuture) - else: - _FUTURE_TYPES = DummyFuture - def add_future(self, future, callback): - """Schedules a callback on the IOLoop when the given future is finished. + """Schedules a callback on the ``IOLoop`` when the given + `.Future` is finished. - The callback is invoked with one argument, the future. + The callback is invoked with one argument, the + `.Future`. """ - assert isinstance(future, IOLoop._FUTURE_TYPES) + assert isinstance(future, Future) callback = stack_context.wrap(callback) future.add_done_callback( lambda future: self.add_callback(callback, future)) @@ -378,14 +455,14 @@ class IOLoop(Configurable): self.handle_callback_exception(callback) def handle_callback_exception(self, callback): - """This method is called whenever a callback run by the IOLoop + """This method is called whenever a callback run by the `IOLoop` throws an exception. By default simply logs the exception as an error. Subclasses may override this method to customize reporting of exceptions. The exception itself is not passed explicitly, but is available - in sys.exc_info. + in `sys.exc_info`. """ app_log.error("Exception in callback %r", callback, exc_info=True) @@ -428,7 +505,11 @@ class PollIOLoop(IOLoop): if all_fds: for fd in self._handlers.keys(): try: - os.close(fd) + close_method = getattr(fd, 'close', None) + if close_method is not None: + close_method() + else: + os.close(fd) except Exception: gen_log.debug("error closing fd %s", fd, exc_info=True) self._waker.close() @@ -684,16 +765,16 @@ class _Timeout(object): class PeriodicCallback(object): """Schedules the given callback to be called periodically. - The callback is called every callback_time milliseconds. + The callback is called every ``callback_time`` milliseconds. - `start` must be called after the PeriodicCallback is created. + `start` must be called after the `PeriodicCallback` is created. """ def __init__(self, callback, callback_time, io_loop=None): self.callback = callback if callback_time <= 0: raise ValueError("Periodic callback must have a positive callback_time") self.callback_time = callback_time - self.io_loop = io_loop or IOLoop.instance() + self.io_loop = io_loop or IOLoop.current() self._running = False self._timeout = None diff --git a/libs/tornado/iostream.py b/libs/tornado/iostream.py index 86cd68a8..16b0fac1 100755 --- a/libs/tornado/iostream.py +++ b/libs/tornado/iostream.py @@ -58,7 +58,7 @@ class BaseIOStream(object): All of the methods take callbacks (since writing and reading are non-blocking and asynchronous). - When a stream is closed due to an error, the IOStream's `error` + When a stream is closed due to an error, the IOStream's ``error`` attribute contains the exception object. Subclasses must implement `fileno`, `close_fd`, `write_to_fd`, @@ -66,7 +66,7 @@ class BaseIOStream(object): """ def __init__(self, io_loop=None, max_buffer_size=104857600, read_chunk_size=4096): - self.io_loop = io_loop or ioloop.IOLoop.instance() + self.io_loop = io_loop or ioloop.IOLoop.current() self.max_buffer_size = max_buffer_size self.read_chunk_size = read_chunk_size self.error = None @@ -110,16 +110,17 @@ class BaseIOStream(object): def read_from_fd(self): """Attempts to read from the underlying file. - Returns ``None`` if there was nothing to read (the socket returned - EWOULDBLOCK or equivalent), otherwise returns the data. When possible, - should return no more than ``self.read_chunk_size`` bytes at a time. + Returns ``None`` if there was nothing to read (the socket + returned `~errno.EWOULDBLOCK` or equivalent), otherwise + returns the data. When possible, should return no more than + ``self.read_chunk_size`` bytes at a time. """ raise NotImplementedError() def get_fd_error(self): """Returns information about any error on the underlying file. - This method is called after the IOLoop has signaled an error on the + This method is called after the `.IOLoop` has signaled an error on the file descriptor, and should return an Exception (such as `socket.error` with additional information, or None if no such information is available. @@ -127,23 +128,32 @@ class BaseIOStream(object): return None def read_until_regex(self, regex, callback): - """Call callback when we read the given regex pattern.""" + """Run ``callback`` when we read the given regex pattern. + + The callback will get the data read (including the data that + matched the regex and anything that came before it) as an argument. + """ self._set_read_callback(callback) self._read_regex = re.compile(regex) self._try_inline_read() def read_until(self, delimiter, callback): - """Call callback when we read the given delimiter.""" + """Run ``callback`` when we read the given delimiter. + + The callback will get the data read (including the delimiter) + as an argument. + """ self._set_read_callback(callback) self._read_delimiter = delimiter self._try_inline_read() def read_bytes(self, num_bytes, callback, streaming_callback=None): - """Call callback when we read the given number of bytes. + """Run callback when we read the given number of bytes. If a ``streaming_callback`` is given, it will be called with chunks of data as they become available, and the argument to the final - ``callback`` will be empty. + ``callback`` will be empty. Otherwise, the ``callback`` gets + the data as an argument. """ self._set_read_callback(callback) assert isinstance(num_bytes, numbers.Integral) @@ -156,7 +166,8 @@ class BaseIOStream(object): If a ``streaming_callback`` is given, it will be called with chunks of data as they become available, and the argument to the final - ``callback`` will be empty. + ``callback`` will be empty. Otherwise, the ``callback`` gets the + data as an argument. Subject to ``max_buffer_size`` limit from `IOStream` constructor if a ``streaming_callback`` is not used. @@ -174,12 +185,12 @@ class BaseIOStream(object): return self._read_until_close = True self._streaming_callback = stack_context.wrap(streaming_callback) - self._add_io_state(self.io_loop.READ) + self._try_inline_read() def write(self, data, callback=None): """Write the given data to this stream. - If callback is given, we call it when all of the buffered write + If ``callback`` is given, we call it when all of the buffered write data has been successfully written to the stream. If there was previously buffered write data and an old write callback, that callback is simply overwritten with this new callback. @@ -213,7 +224,7 @@ class BaseIOStream(object): """Close this stream. If ``exc_info`` is true, set the ``error`` attribute to the current - exception from `sys.exc_info()` (or if ``exc_info`` is a tuple, + exception from `sys.exc_info` (or if ``exc_info`` is a tuple, use that instead of `sys.exc_info`). """ if not self.closed(): @@ -573,44 +584,45 @@ class BaseIOStream(object): class IOStream(BaseIOStream): - r"""Socket-based IOStream implementation. + r"""Socket-based `IOStream` implementation. This class supports the read and write methods from `BaseIOStream` plus a `connect` method. - The socket parameter may either be connected or unconnected. For - server operations the socket is the result of calling socket.accept(). - For client operations the socket is created with socket.socket(), - and may either be connected before passing it to the IOStream or - connected with IOStream.connect. + The ``socket`` parameter may either be connected or unconnected. + For server operations the socket is the result of calling + `socket.accept `. For client operations the + socket is created with `socket.socket`, and may either be + connected before passing it to the `IOStream` or connected with + `IOStream.connect`. A very simple (and broken) HTTP client using this class:: - from tornado import ioloop - from tornado import iostream + import tornado.ioloop + import tornado.iostream import socket def send_request(): - stream.write("GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n") - stream.read_until("\r\n\r\n", on_headers) + stream.write(b"GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n") + stream.read_until(b"\r\n\r\n", on_headers) def on_headers(data): headers = {} - for line in data.split("\r\n"): - parts = line.split(":") + for line in data.split(b"\r\n"): + parts = line.split(b":") if len(parts) == 2: headers[parts[0].strip()] = parts[1].strip() - stream.read_bytes(int(headers["Content-Length"]), on_body) + stream.read_bytes(int(headers[b"Content-Length"]), on_body) def on_body(data): print data stream.close() - ioloop.IOLoop.instance().stop() + tornado.ioloop.IOLoop.instance().stop() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) - stream = iostream.IOStream(s) + stream = tornado.iostream.IOStream(s) stream.connect(("friendfeed.com", 80), send_request) - ioloop.IOLoop.instance().start() + tornado.ioloop.IOLoop.instance().start() """ def __init__(self, socket, *args, **kwargs): self.socket = socket @@ -650,20 +662,20 @@ class IOStream(BaseIOStream): May only be called if the socket passed to the constructor was not previously connected. The address parameter is in the - same format as for socket.connect, i.e. a (host, port) tuple. - If callback is specified, it will be called when the - connection is completed. + same format as for `socket.connect `, + i.e. a ``(host, port)`` tuple. If ``callback`` is specified, + it will be called when the connection is completed. If specified, the ``server_hostname`` parameter will be used in SSL connections for certificate validation (if requested in the ``ssl_options``) and SNI (if supported; requires Python 3.2+). - Note that it is safe to call IOStream.write while the - connection is pending, in which case the data will be written - as soon as the connection is ready. Calling IOStream read - methods before the socket is connected works on some platforms - but is non-portable. + Note that it is safe to call `IOStream.write + ` while the connection is pending, in + which case the data will be written as soon as the connection + is ready. Calling `IOStream` read methods before the socket is + connected works on some platforms but is non-portable. """ self._connecting = True try: @@ -711,13 +723,11 @@ class SSLIOStream(IOStream): ssl.wrap_socket(sock, do_handshake_on_connect=False, **kwargs) - before constructing the SSLIOStream. Unconnected sockets will be - wrapped when IOStream.connect is finished. + before constructing the `SSLIOStream`. Unconnected sockets will be + wrapped when `IOStream.connect` is finished. """ def __init__(self, *args, **kwargs): - """Creates an SSLIOStream. - - The ``ssl_options`` keyword argument may either be a dictionary + """The ``ssl_options`` keyword argument may either be a dictionary of keywords arguments for `ssl.wrap_socket`, or an `ssl.SSLContext` object. """ @@ -863,10 +873,12 @@ class SSLIOStream(IOStream): class PipeIOStream(BaseIOStream): - """Pipe-based IOStream implementation. + """Pipe-based `IOStream` implementation. The constructor takes an integer file descriptor (such as one returned - by `os.pipe`) rather than an open file object. + by `os.pipe`) rather than an open file object. Pipes are generally + one-way, so a `PipeIOStream` can be used for reading or writing but not + both. """ def __init__(self, fd, *args, **kwargs): self.fd = fd diff --git a/libs/tornado/locale.py b/libs/tornado/locale.py index e4e1a154..66e9ff6d 100755 --- a/libs/tornado/locale.py +++ b/libs/tornado/locale.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# +# -*- coding: utf-8 -*- # Copyright 2009 Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -18,25 +18,25 @@ To load a locale and generate a translated string:: - user_locale = locale.get("es_LA") + user_locale = tornado.locale.get("es_LA") print user_locale.translate("Sign out") -locale.get() returns the closest matching locale, not necessarily the +`tornado.locale.get()` returns the closest matching locale, not necessarily the specific locale you requested. You can support pluralization with -additional arguments to translate(), e.g.:: +additional arguments to `~Locale.translate()`, e.g.:: people = [...] message = user_locale.translate( "%(list)s is online", "%(list)s are online", len(people)) print message % {"list": user_locale.list(people)} -The first string is chosen if len(people) == 1, otherwise the second +The first string is chosen if ``len(people) == 1``, otherwise the second string is chosen. -Applications should call one of load_translations (which uses a simple -CSV format) or load_gettext_translations (which uses the .mo format -supported by gettext and related tools). If neither method is called, -the locale.translate method will simply return the original string. +Applications should call one of `load_translations` (which uses a simple +CSV format) or `load_gettext_translations` (which uses the ``.mo`` format +supported by `gettext` and related tools). If neither method is called, +the `Locale.translate` method will simply return the original string. """ from __future__ import absolute_import, division, print_function, with_statement @@ -63,15 +63,15 @@ def get(*locale_codes): or a loose match for the code (e.g., "en" for "en_US"), we return the locale. Otherwise we move to the next code in the list. - By default we return en_US if no translations are found for any of + By default we return ``en_US`` if no translations are found for any of the specified locales. You can change the default locale with - set_default_locale() below. + `set_default_locale()`. """ return Locale.get_closest(*locale_codes) def set_default_locale(code): - """Sets the default locale, used in get_closest_locale(). + """Sets the default locale. The default locale is assumed to be the language used for all strings in the system. The translations loaded from disk are mappings from @@ -85,32 +85,32 @@ def set_default_locale(code): def load_translations(directory): - u("""Loads translations from CSV files in a directory. + """Loads translations from CSV files in a directory. Translations are strings with optional Python-style named placeholders - (e.g., "My name is %(name)s") and their associated translations. + (e.g., ``My name is %(name)s``) and their associated translations. - The directory should have translation files of the form LOCALE.csv, - e.g. es_GT.csv. The CSV files should have two or three columns: string, + The directory should have translation files of the form ``LOCALE.csv``, + e.g. ``es_GT.csv``. The CSV files should have two or three columns: string, translation, and an optional plural indicator. Plural indicators should be one of "plural" or "singular". A given string can have both singular - and plural forms. For example "%(name)s liked this" may have a + and plural forms. For example ``%(name)s liked this`` may have a different verb conjugation depending on whether %(name)s is one name or a list of names. There should be two rows in the CSV file for that string, one with plural indicator "singular", and one "plural". For strings with no verbs that would change on translation, simply use "unknown" or the empty string (or don't include the column at all). - The file is read using the csv module in the default "excel" dialect. + The file is read using the `csv` module in the default "excel" dialect. In this format there should not be spaces after the commas. - Example translation es_LA.csv: + Example translation ``es_LA.csv``:: "I love you","Te amo" - "%(name)s liked this","A %(name)s les gust\u00f3 esto","plural" - "%(name)s liked this","A %(name)s le gust\u00f3 esto","singular" + "%(name)s liked this","A %(name)s les gustó esto","plural" + "%(name)s liked this","A %(name)s le gustó esto","singular" - """) + """ global _translations global _supported_locales _translations = {} @@ -151,22 +151,25 @@ def load_translations(directory): def load_gettext_translations(directory, domain): - """Loads translations from gettext's locale tree + """Loads translations from `gettext`'s locale tree - Locale tree is similar to system's /usr/share/locale, like: + Locale tree is similar to system's ``/usr/share/locale``, like:: - {directory}/{lang}/LC_MESSAGES/{domain}.mo + {directory}/{lang}/LC_MESSAGES/{domain}.mo Three steps are required to have you app translated: - 1. Generate POT translation file - xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py file2.html etc + 1. Generate POT translation file:: - 2. Merge against existing POT file: - msgmerge old.po cyclone.po > new.po + xgettext --language=Python --keyword=_:1,2 -d mydomain file1.py file2.html etc - 3. Compile: - msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo + 2. Merge against existing POT file:: + + msgmerge old.po mydomain.po > new.po + + 3. Compile:: + + msgfmt mydomain.po -o {directory}/pt_BR/LC_MESSAGES/mydomain.mo """ import gettext global _translations @@ -262,9 +265,10 @@ class Locale(object): def translate(self, message, plural_message=None, count=None): """Returns the translation for the given message for this locale. - If plural_message is given, you must also provide count. We return - plural_message when count != 1, and we return the singular form - for the given message when count == 1. + If ``plural_message`` is given, you must also provide + ``count``. We return ``plural_message`` when ``count != 1``, + and we return the singular form for the given message when + ``count == 1``. """ raise NotImplementedError() @@ -273,10 +277,10 @@ class Locale(object): """Formats the given date (which should be GMT). By default, we return a relative time (e.g., "2 minutes ago"). You - can return an absolute date string with relative=False. + can return an absolute date string with ``relative=False``. You can force a full format date ("July 10, 1980") with - full_format=True. + ``full_format=True``. This method is primarily intended for dates in the past. For dates in the future, we fall back to full format. @@ -360,7 +364,7 @@ class Locale(object): """Formats the given date as a day of week. Example: "Monday, January 22". You can remove the day of week with - dow=False. + ``dow=False``. """ local_date = date - datetime.timedelta(minutes=gmt_offset) _ = self.translate @@ -421,7 +425,7 @@ class CSVLocale(Locale): class GettextLocale(Locale): - """Locale implementation using the gettext module.""" + """Locale implementation using the `gettext` module.""" def __init__(self, code, translations): try: # python 2 diff --git a/libs/tornado/netutil.py b/libs/tornado/netutil.py index 4003245e..7b7d48dd 100755 --- a/libs/tornado/netutil.py +++ b/libs/tornado/netutil.py @@ -41,14 +41,14 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags Address may be either an IP address or hostname. If it's a hostname, the server will listen on all IP addresses associated with the name. Address may be an empty string or None to listen on all - available interfaces. Family may be set to either socket.AF_INET - or socket.AF_INET6 to restrict to ipv4 or ipv6 addresses, otherwise + available interfaces. Family may be set to either `socket.AF_INET` + or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise both will be used if available. The ``backlog`` argument has the same meaning as for - ``socket.listen()``. + `socket.listen() `. - ``flags`` is a bitmask of AI_* flags to ``getaddrinfo``, like + ``flags`` is a bitmask of AI_* flags to `~socket.getaddrinfo`, like ``socket.AI_PASSIVE | socket.AI_NUMERICHOST``. """ sockets = [] @@ -119,16 +119,16 @@ if hasattr(socket, 'AF_UNIX'): def add_accept_handler(sock, callback, io_loop=None): - """Adds an ``IOLoop`` event handler to accept new connections on ``sock``. + """Adds an `.IOLoop` event handler to accept new connections on ``sock``. When a connection is accepted, ``callback(connection, address)`` will be run (``connection`` is a socket object, and ``address`` is the address of the other end of the connection). Note that this signature is different from the ``callback(fd, events)`` signature used for - ``IOLoop`` handlers. + `.IOLoop` handlers. """ if io_loop is None: - io_loop = IOLoop.instance() + io_loop = IOLoop.current() def accept_handler(fd, events): while True: @@ -142,7 +142,41 @@ def add_accept_handler(sock, callback, io_loop=None): io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ) +def is_valid_ip(ip): + """Returns true if the given string is a well-formed IP address. + + Supports IPv4 and IPv6. + """ + try: + res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC, + socket.SOCK_STREAM, + 0, socket.AI_NUMERICHOST) + return bool(res) + except socket.gaierror as e: + if e.args[0] == socket.EAI_NONAME: + return False + raise + return True + + class Resolver(Configurable): + """Configurable asynchronous DNS resolver interface. + + By default, a blocking implementation is used (which simply calls + `socket.getaddrinfo`). An alternative implementation can be + chosen with the `Resolver.configure <.Configurable.configure>` + class method:: + + Resolver.configure('tornado.netutil.ThreadedResolver') + + The implementations of this interface included with Tornado are + + * `tornado.netutil.BlockingResolver` + * `tornado.netutil.ThreadedResolver` + * `tornado.netutil.OverrideResolver` + * `tornado.platform.twisted.TwistedResolver` + * `tornado.platform.caresresolver.CaresResolver` + """ @classmethod def configurable_base(cls): return Resolver @@ -151,40 +185,64 @@ class Resolver(Configurable): def configurable_default(cls): return BlockingResolver - def getaddrinfo(self, *args, **kwargs): + def resolve(self, host, port, family=socket.AF_UNSPEC, callback=None): """Resolves an address. - The arguments to this function are the same as to - `socket.getaddrinfo`, with the addition of an optional - keyword-only ``callback`` argument. + The ``host`` argument is a string which may be a hostname or a + literal IP address. - Returns a `Future` whose result is the same as the return - value of `socket.getaddrinfo`. If a callback is passed, - it will be run with the `Future` as an argument when it - is complete. + Returns a `.Future` whose result is a list of (family, + address) pairs, where address is a tuple suitable to pass to + `socket.connect ` (i.e. a ``(host, + port)`` pair for IPv4; additional fields may be present for + IPv6). If a ``callback`` is passed, it will be run with the + result as an argument when it is complete. """ raise NotImplementedError() class ExecutorResolver(Resolver): def initialize(self, io_loop=None, executor=None): - self.io_loop = io_loop or IOLoop.instance() + self.io_loop = io_loop or IOLoop.current() self.executor = executor or dummy_executor @run_on_executor - def getaddrinfo(self, *args, **kwargs): - return socket.getaddrinfo(*args, **kwargs) + def resolve(self, host, port, family=socket.AF_UNSPEC): + addrinfo = socket.getaddrinfo(host, port, family) + results = [] + for family, socktype, proto, canonname, address in addrinfo: + results.append((family, address)) + return results + class BlockingResolver(ExecutorResolver): + """Default `Resolver` implementation, using `socket.getaddrinfo`. + + The `.IOLoop` will be blocked during the resolution, although the + callback will not be run until the next `.IOLoop` iteration. + """ def initialize(self, io_loop=None): super(BlockingResolver, self).initialize(io_loop=io_loop) + class ThreadedResolver(ExecutorResolver): + """Multithreaded non-blocking `Resolver` implementation. + + Requires the `concurrent.futures` package to be installed + (available in the standard library since Python 3.2, + installable with ``pip install futures`` in older versions). + + The thread pool size can be configured with:: + + Resolver.configure('tornado.netutil.ThreadedResolver', + num_threads=10) + """ def initialize(self, io_loop=None, num_threads=10): from concurrent.futures import ThreadPoolExecutor super(ThreadedResolver, self).initialize( io_loop=io_loop, executor=ThreadPoolExecutor(num_threads)) + class OverrideResolver(Resolver): """Wraps a resolver with a mapping of overrides. @@ -197,13 +255,12 @@ class OverrideResolver(Resolver): self.resolver = resolver self.mapping = mapping - def getaddrinfo(self, host, port, *args, **kwargs): + def resolve(self, host, port, *args, **kwargs): if (host, port) in self.mapping: host, port = self.mapping[(host, port)] elif host in self.mapping: host = self.mapping[host] - return self.resolver.getaddrinfo(host, port, *args, **kwargs) - + return self.resolver.resolve(host, port, *args, **kwargs) # These are the keyword arguments to ssl.wrap_socket that must be translated @@ -212,20 +269,22 @@ class OverrideResolver(Resolver): _SSL_CONTEXT_KEYWORDS = frozenset(['ssl_version', 'certfile', 'keyfile', 'cert_reqs', 'ca_certs', 'ciphers']) + def ssl_options_to_context(ssl_options): - """Try to Convert an ssl_options dictionary to an SSLContext object. + """Try to convert an ``ssl_options`` dictionary to an + `~ssl.SSLContext` object. The ``ssl_options`` dictionary contains keywords to be passed to - `ssl.wrap_sockets`. In Python 3.2+, `ssl.SSLContext` objects can + `ssl.wrap_socket`. In Python 3.2+, `ssl.SSLContext` objects can be used instead. This function converts the dict form to its - `SSLContext` equivalent, and may be used when a component which - accepts both forms needs to upgrade to the `SSLContext` version + `~ssl.SSLContext` equivalent, and may be used when a component which + accepts both forms needs to upgrade to the `~ssl.SSLContext` version to use features like SNI or NPN. """ if isinstance(ssl_options, dict): assert all(k in _SSL_CONTEXT_KEYWORDS for k in ssl_options), ssl_options if (not hasattr(ssl, 'SSLContext') or - isinstance(ssl_options, ssl.SSLContext)): + isinstance(ssl_options, ssl.SSLContext)): return ssl_options context = ssl.SSLContext( ssl_options.get('ssl_version', ssl.PROTOCOL_SSLv23)) @@ -241,12 +300,12 @@ def ssl_options_to_context(ssl_options): def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs): - """Returns an `ssl.SSLSocket` wrapping the given socket. + """Returns an ``ssl.SSLSocket`` wrapping the given socket. ``ssl_options`` may be either a dictionary (as accepted by - `ssl_options_to_context) or an `ssl.SSLContext` object. - Additional keyword arguments are passed to `wrap_socket` - (either the `SSLContext` method or the `ssl` module function + `ssl_options_to_context`) or an `ssl.SSLContext` object. + Additional keyword arguments are passed to ``wrap_socket`` + (either the `~ssl.SSLContext` method or the `ssl` module function as appropriate). """ context = ssl_options_to_context(ssl_options) @@ -262,7 +321,7 @@ def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs): else: return ssl.wrap_socket(socket, **dict(context, **kwargs)) -if hasattr(ssl, 'match_hostname'): # python 3.2+ +if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ ssl_match_hostname = ssl.match_hostname SSLCertificateError = ssl.CertificateError else: @@ -272,7 +331,6 @@ else: class SSLCertificateError(ValueError): pass - def _dnsname_to_pat(dn): pats = [] for frag in dn.split(r'.'): @@ -286,7 +344,6 @@ else: pats.append(frag.replace(r'\*', '[^.]*')) return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) - def ssl_match_hostname(cert, hostname): """Verify that *cert* (in decoded format as returned by SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules diff --git a/libs/tornado/options.py b/libs/tornado/options.py index ee146fca..b96f815d 100755 --- a/libs/tornado/options.py +++ b/libs/tornado/options.py @@ -29,27 +29,28 @@ option namespace, e.g.:: db = database.Connection(options.mysql_host) ... -The main() method of your application does not need to be aware of all of +The ``main()`` method of your application does not need to be aware of all of the options used throughout your program; they are all automatically loaded when the modules are loaded. However, all modules that define options must have been imported before the command line is parsed. -Your main() method can parse the command line or parse a config file with +Your ``main()`` method can parse the command line or parse a config file with either:: tornado.options.parse_command_line() # or tornado.options.parse_config_file("/etc/server.conf") -Command line formats are what you would expect ("--myoption=myvalue"). +Command line formats are what you would expect (``--myoption=myvalue``). Config files are just Python files. Global names become options, e.g.:: myoption = "myvalue" myotheroption = "myothervalue" -We support datetimes, timedeltas, ints, and floats (just pass a 'type' -kwarg to define). We also accept multi-value options. See the documentation -for define() below. +We support `datetimes `, `timedeltas +`, ints, and floats (just pass a ``type`` kwarg to +`define`). We also accept multi-value options. See the documentation for +`define()` below. `tornado.options.options` is a singleton instance of `OptionParser`, and the top-level functions in this module (`define`, `parse_command_line`, etc) @@ -68,7 +69,7 @@ import textwrap from tornado.escape import _unicode from tornado.log import define_logging_options from tornado import stack_context -from tornado.util import basestring_type +from tornado.util import basestring_type, exec_in class Error(Exception): @@ -103,28 +104,29 @@ class OptionParser(object): multiple=False, group=None, callback=None): """Defines a new command line option. - If type is given (one of str, float, int, datetime, or timedelta) - or can be inferred from the default, we parse the command line - arguments based on the given type. If multiple is True, we accept + If ``type`` is given (one of str, float, int, datetime, or timedelta) + or can be inferred from the ``default``, we parse the command line + arguments based on the given type. If ``multiple`` is True, we accept comma-separated values, and the option value is always a list. - For multi-value integers, we also accept the syntax x:y, which - turns into range(x, y) - very useful for long integer ranges. + For multi-value integers, we also accept the syntax ``x:y``, which + turns into ``range(x, y)`` - very useful for long integer ranges. - help and metavar are used to construct the automatically generated - command line help string. The help message is formatted like:: + ``help`` and ``metavar`` are used to construct the + automatically generated command line help string. The help + message is formatted like:: --name=METAVAR help string - group is used to group the defined options in logical + ``group`` is used to group the defined options in logical groups. By default, command line options are grouped by the file in which they are defined. Command line option names must be unique globally. They can be parsed - from the command line with parse_command_line() or parsed from a - config file with parse_config_file. + from the command line with `parse_command_line` or parsed from a + config file with `parse_config_file`. - If a callback is given, it will be run with the new value whenever + If a ``callback`` is given, it will be run with the new value whenever the option is changed. This can be used to combine command-line and file-based options:: @@ -159,9 +161,11 @@ class OptionParser(object): callback=callback) def parse_command_line(self, args=None, final=True): - """Parses all options given on the command line (defaults to sys.argv). + """Parses all options given on the command line (defaults to + `sys.argv`). - Note that args[0] is ignored since it is the program name in sys.argv. + Note that ``args[0]`` is ignored since it is the program name + in `sys.argv`. We return a list of all arguments that are not parsed as options. @@ -207,7 +211,8 @@ class OptionParser(object): from multiple sources. """ config = {} - execfile(path, config, config) + with open(path) as f: + exec_in(f.read(), config, config) for name in config: if name in self._options: self._options[name].set(config[name]) @@ -258,15 +263,16 @@ class OptionParser(object): callback() def mockable(self): - """Returns a wrapper around self that is compatible with `mock.patch`. + """Returns a wrapper around self that is compatible with + `mock.patch `. - The `mock.patch` function (included in the standard library - `unittest.mock` package since Python 3.3, or in the - third-party `mock` package for older versions of Python) is - incompatible with objects like ``options`` that override - ``__getattr__`` and ``__setattr__``. This function returns an - object that can be used with `mock.patch.object` to modify - option values:: + The `mock.patch ` function (included in + the standard library `unittest.mock` package since Python 3.3, + or in the third-party ``mock`` package for older versions of + Python) is incompatible with objects like ``options`` that + override ``__getattr__`` and ``__setattr__``. This function + returns an object that can be used with `mock.patch.object + ` to modify option values:: with mock.patch.object(options.mockable(), 'name', value): assert options.name == value diff --git a/libs/tornado/platform/caresresolver.py b/libs/tornado/platform/caresresolver.py new file mode 100755 index 00000000..7c16705d --- /dev/null +++ b/libs/tornado/platform/caresresolver.py @@ -0,0 +1,75 @@ +import pycares +import socket + +from tornado import gen +from tornado.ioloop import IOLoop +from tornado.netutil import Resolver, is_valid_ip + + +class CaresResolver(Resolver): + """Name resolver based on the c-ares library. + + This is a non-blocking and non-threaded resolver. It may not produce + the same results as the system resolver, but can be used for non-blocking + resolution when threads cannot be used. + + c-ares fails to resolve some names when ``family`` is ``AF_UNSPEC``, + so it is only recommended for use in ``AF_INET`` (i.e. IPv4). This is + the default for ``tornado.simple_httpclient``, but other libraries + may default to ``AF_UNSPEC``. + """ + def initialize(self, io_loop=None): + self.io_loop = io_loop or IOLoop.current() + self.channel = pycares.Channel(sock_state_cb=self._sock_state_cb) + self.fds = {} + + def _sock_state_cb(self, fd, readable, writable): + state = ((IOLoop.READ if readable else 0) | + (IOLoop.WRITE if writable else 0)) + if not state: + self.io_loop.remove_handler(fd) + del self.fds[fd] + elif fd in self.fds: + self.io_loop.update_handler(fd, state) + self.fds[fd] = state + else: + self.io_loop.add_handler(fd, self._handle_events, state) + self.fds[fd] = state + + def _handle_events(self, fd, events): + read_fd = pycares.ARES_SOCKET_BAD + write_fd = pycares.ARES_SOCKET_BAD + if events & IOLoop.READ: + read_fd = fd + if events & IOLoop.WRITE: + write_fd = fd + self.channel.process_fd(read_fd, write_fd) + + @gen.coroutine + def resolve(self, host, port, family=0): + if is_valid_ip(host): + addresses = [host] + else: + # gethostbyname doesn't take callback as a kwarg + self.channel.gethostbyname(host, family, (yield gen.Callback(1))) + callback_args = yield gen.Wait(1) + assert isinstance(callback_args, gen.Arguments) + assert not callback_args.kwargs + result, error = callback_args.args + if error: + raise Exception('C-Ares returned error %s: %s while resolving %s' % + (error, pycares.errno.strerror(error), host)) + addresses = result.addresses + addrinfo = [] + for address in addresses: + if '.' in address: + address_family = socket.AF_INET + elif ':' in address: + address_family = socket.AF_INET6 + else: + address_family = socket.AF_UNSPEC + if family != socket.AF_UNSPEC and family != address_family: + raise Exception('Requested socket family %d but got %d' % + (family, address_family)) + addrinfo.append((address_family, (address, port))) + raise gen.Return(addrinfo) diff --git a/libs/tornado/platform/twisted.py b/libs/tornado/platform/twisted.py index 34e108d7..910e46af 100755 --- a/libs/tornado/platform/twisted.py +++ b/libs/tornado/platform/twisted.py @@ -22,6 +22,8 @@ This module lets you run applications and libraries written for Twisted in a Tornado application. It can be used in two modes, depending on which library's underlying event loop you want to use. +This module has been tested with Twisted versions 11.0.0 and newer. + Twisted on Tornado ------------------ @@ -60,26 +62,33 @@ reactor. Recommended usage:: reactor.run() `TwistedIOLoop` always uses the global Twisted reactor. - -This module has been tested with Twisted versions 11.0.0 and newer. """ from __future__ import absolute_import, division, print_function, with_statement -import functools import datetime +import functools +import socket +import twisted.internet.abstract from twisted.internet.posixbase import PosixReactorBase from twisted.internet.interfaces import \ IReactorFDSet, IDelayedCall, IReactorTime, IReadDescriptor, IWriteDescriptor from twisted.python import failure, log from twisted.internet import error +import twisted.names.cache +import twisted.names.client +import twisted.names.hosts +import twisted.names.resolve from zope.interface import implementer -import tornado +from tornado.concurrent import return_future +from tornado.escape import utf8 +from tornado import gen import tornado.ioloop from tornado.log import app_log +from tornado.netutil import Resolver from tornado.stack_context import NullContext, wrap from tornado.ioloop import IOLoop @@ -140,7 +149,7 @@ class TornadoReactor(PosixReactorBase): """ def __init__(self, io_loop=None): if not io_loop: - io_loop = tornado.ioloop.IOLoop.instance() + io_loop = tornado.ioloop.IOLoop.current() self._io_loop = io_loop self._readers = {} # map of reader objects to fd self._writers = {} # map of writer objects to fd @@ -177,8 +186,12 @@ class TornadoReactor(PosixReactorBase): def callFromThread(self, f, *args, **kw): """See `twisted.internet.interfaces.IReactorThreads.callFromThread`""" assert callable(f), "%s is not callable" % f - p = functools.partial(f, *args, **kw) - self._io_loop.add_callback(p) + with NullContext(): + # This NullContext is mainly for an edge case when running + # TwistedIOLoop on top of a TornadoReactor. + # TwistedIOLoop.add_callback uses reactor.callFromThread and + # should not pick up additional StackContexts along the way. + self._io_loop.add_callback(f, *args, **kw) # We don't need the waker code from the super class, Tornado uses # its own waker. @@ -344,7 +357,7 @@ class _TestReactor(TornadoReactor): def install(io_loop=None): """Install this package as the default Twisted reactor.""" if not io_loop: - io_loop = tornado.ioloop.IOLoop.instance() + io_loop = tornado.ioloop.IOLoop.current() reactor = TornadoReactor(io_loop) from twisted.internet.main import installReactor installReactor(reactor) @@ -383,18 +396,21 @@ class _FD(object): class TwistedIOLoop(tornado.ioloop.IOLoop): """IOLoop implementation that runs on Twisted. - Uses the global Twisted reactor. It is possible to create multiple - TwistedIOLoops in the same process, but it doesn't really make sense - because they will all run in the same thread. + Uses the global Twisted reactor by default. To create multiple + `TwistedIOLoops` in the same process, you must pass a unique reactor + when constructing each one. Not compatible with `tornado.process.Subprocess.set_exit_callback` because the ``SIGCHLD`` handlers used by Tornado and Twisted conflict with each other. """ - def initialize(self): - from twisted.internet import reactor + def initialize(self, reactor=None): + if reactor is None: + import twisted.internet.reactor + reactor = twisted.internet.reactor self.reactor = reactor self.fds = {} + self.reactor.callWhenRunning(self.make_current) def close(self, all_fds=False): self.reactor.removeAll() @@ -456,13 +472,14 @@ class TwistedIOLoop(tornado.ioloop.IOLoop): if isinstance(deadline, (int, long, float)): delay = max(deadline - self.time(), 0) elif isinstance(deadline, datetime.timedelta): - delay = deadline.total_seconds() + delay = tornado.ioloop._Timeout.timedelta_to_seconds(deadline) else: raise TypeError("Unsupported deadline %r") return self.reactor.callLater(delay, self._run_callback, wrap(callback)) def remove_timeout(self, timeout): - timeout.cancel() + if timeout.active(): + timeout.cancel() def add_callback(self, callback, *args, **kwargs): self.reactor.callFromThread(self._run_callback, @@ -470,3 +487,58 @@ class TwistedIOLoop(tornado.ioloop.IOLoop): def add_callback_from_signal(self, callback, *args, **kwargs): self.add_callback(callback, *args, **kwargs) + + +class TwistedResolver(Resolver): + """Twisted-based asynchronous resolver. + + This is a non-blocking and non-threaded resolver. It is + recommended only when threads cannot be used, since it has + limitations compared to the standard ``getaddrinfo``-based + `~tornado.netutil.Resolver` and + `~tornado.netutil.ThreadedResolver`. Specifically, it returns at + most one result, and arguments other than ``host`` and ``family`` + are ignored. It may fail to resolve when ``family`` is not + ``socket.AF_UNSPEC``. + + Requires Twisted 12.1 or newer. + """ + def initialize(self, io_loop=None): + self.io_loop = io_loop or IOLoop.current() + # partial copy of twisted.names.client.createResolver, which doesn't + # allow for a reactor to be passed in. + self.reactor = tornado.platform.twisted.TornadoReactor(io_loop) + + host_resolver = twisted.names.hosts.Resolver('/etc/hosts') + cache_resolver = twisted.names.cache.CacheResolver(reactor=self.reactor) + real_resolver = twisted.names.client.Resolver('/etc/resolv.conf', + reactor=self.reactor) + self.resolver = twisted.names.resolve.ResolverChain( + [host_resolver, cache_resolver, real_resolver]) + + @gen.coroutine + def resolve(self, host, port, family=0): + # getHostByName doesn't accept IP addresses, so if the input + # looks like an IP address just return it immediately. + if twisted.internet.abstract.isIPAddress(host): + resolved = host + resolved_family = socket.AF_INET + elif twisted.internet.abstract.isIPv6Address(host): + resolved = host + resolved_family = socket.AF_INET6 + else: + deferred = self.resolver.getHostByName(utf8(host)) + resolved = yield gen.Task(deferred.addCallback) + if twisted.internet.abstract.isIPAddress(resolved): + resolved_family = socket.AF_INET + elif twisted.internet.abstract.isIPv6Address(resolved): + resolved_family = socket.AF_INET6 + else: + resolved_family = socket.AF_UNSPEC + if family != socket.AF_UNSPEC and family != resolved_family: + raise Exception('Requested socket family %d but got %d' % + (family, resolved_family)) + result = [ + (resolved_family, (resolved, port)), + ] + raise gen.Return(result) diff --git a/libs/tornado/process.py b/libs/tornado/process.py index 0509eb3a..438db66d 100755 --- a/libs/tornado/process.py +++ b/libs/tornado/process.py @@ -14,7 +14,9 @@ # License for the specific language governing permissions and limitations # under the License. -"""Utilities for working with multiple processes.""" +"""Utilities for working with multiple processes, including both forking +the server into multiple processes and managing subprocesses. +""" from __future__ import absolute_import, division, print_function, with_statement @@ -169,8 +171,8 @@ class Subprocess(object): additions: * ``stdin``, ``stdout``, and ``stderr`` may have the value - `tornado.process.Subprocess.STREAM`, which will make the corresponding - attribute of the resulting Subprocess a `PipeIOStream`. + ``tornado.process.Subprocess.STREAM``, which will make the corresponding + attribute of the resulting Subprocess a `.PipeIOStream`. * A new keyword argument ``io_loop`` may be used to pass in an IOLoop. """ STREAM = object() @@ -229,15 +231,15 @@ class Subprocess(object): def initialize(cls, io_loop=None): """Initializes the ``SIGCHILD`` handler. - The signal handler is run on an IOLoop to avoid locking issues. - Note that the IOLoop used for signal handling need not be the + The signal handler is run on an `.IOLoop` to avoid locking issues. + Note that the `.IOLoop` used for signal handling need not be the same one used by individual Subprocess objects (as long as the - IOLoops are each running in separate threads). + ``IOLoops`` are each running in separate threads). """ if cls._initialized: return if io_loop is None: - io_loop = ioloop.IOLoop.instance() + io_loop = ioloop.IOLoop.current() cls._old_sigchld = signal.signal( signal.SIGCHLD, lambda sig, frame: io_loop.add_callback_from_signal(cls._cleanup)) diff --git a/libs/tornado/simple_httpclient.py b/libs/tornado/simple_httpclient.py index f33ed242..117ce75b 100755 --- a/libs/tornado/simple_httpclient.py +++ b/libs/tornado/simple_httpclient.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, with_statement from tornado.escape import utf8, _unicode, native_str -from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy +from tornado.httpclient import HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy from tornado.httputil import HTTPHeaders from tornado.iostream import IOStream, SSLIOStream from tornado.netutil import Resolver, OverrideResolver @@ -92,10 +92,12 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): request, callback = self.queue.popleft() key = object() self.active[key] = (request, callback) - _HTTPConnection(self.io_loop, self, request, - functools.partial(self._release_fetch, key), - callback, - self.max_buffer_size, self.resolver) + release_callback = functools.partial(self._release_fetch, key) + self._handle_request(request, release_callback, callback) + + def _handle_request(self, request, release_callback, final_callback): + _HTTPConnection(self.io_loop, self, request, release_callback, + final_callback, self.max_buffer_size, self.resolver) def _release_fetch(self, key): del self.active[key] @@ -150,13 +152,24 @@ class _HTTPConnection(object): # so restrict to ipv4 by default. af = socket.AF_INET - self.resolver.getaddrinfo( - host, port, af, socket.SOCK_STREAM, 0, 0, - callback=self._on_resolve) + self.resolver.resolve(host, port, af, callback=self._on_resolve) - def _on_resolve(self, future): - af, socktype, proto, canonname, sockaddr = future.result()[0] + def _on_resolve(self, addrinfo): + self.stream = self._create_stream(addrinfo) + timeout = min(self.request.connect_timeout, self.request.request_timeout) + if timeout: + self._timeout = self.io_loop.add_timeout( + self.start_time + timeout, + stack_context.wrap(self._on_timeout)) + self.stream.set_close_callback(self._on_close) + # ipv6 addresses are broken (in self.parsed.hostname) until + # 2.7, here is correctly parsed value calculated in __init__ + sockaddr = addrinfo[0][1] + self.stream.connect(sockaddr, self._on_connect, + server_hostname=self.parsed_hostname) + def _create_stream(self, addrinfo): + af = addrinfo[0][0] if self.parsed.scheme == "https": ssl_options = {} if self.request.validate_cert: @@ -189,24 +202,14 @@ class _HTTPConnection(object): # information. ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3 - self.stream = SSLIOStream(socket.socket(af, socktype, proto), - io_loop=self.io_loop, - ssl_options=ssl_options, - max_buffer_size=self.max_buffer_size) + return SSLIOStream(socket.socket(af), + io_loop=self.io_loop, + ssl_options=ssl_options, + max_buffer_size=self.max_buffer_size) else: - self.stream = IOStream(socket.socket(af, socktype, proto), - io_loop=self.io_loop, - max_buffer_size=self.max_buffer_size) - timeout = min(self.request.connect_timeout, self.request.request_timeout) - if timeout: - self._timeout = self.io_loop.add_timeout( - self.start_time + timeout, - stack_context.wrap(self._on_timeout)) - self.stream.set_close_callback(self._on_close) - # ipv6 addresses are broken (in self.parsed.hostname) until - # 2.7, here is correctly parsed value calculated in __init__ - self.stream.connect(sockaddr, self._on_connect, - server_hostname=self.parsed_hostname) + return IOStream(socket.socket(af), + io_loop=self.io_loop, + max_buffer_size=self.max_buffer_size) def _on_timeout(self): self._timeout = None @@ -246,6 +249,9 @@ class _HTTPConnection(object): username = self.request.auth_username password = self.request.auth_password or '' if username is not None: + if self.request.auth_mode not in (None, "basic"): + raise ValueError("unsupported auth_mode %s", + self.request.auth_mode) auth = utf8(username) + b":" + utf8(password) self.request.headers["Authorization"] = (b"Basic " + base64.b64encode(auth)) @@ -414,7 +420,7 @@ class _HTTPConnection(object): self.final_callback = None self._release() self.client.fetch(new_request, final_callback) - self.stream.close() + self._on_end_request() return if self._decompressor: data = (self._decompressor.decompress(data) + @@ -434,6 +440,9 @@ class _HTTPConnection(object): buffer=buffer, effective_url=self.request.url) self._run_callback(response) + self._on_end_request() + + def _on_end_request(self): self.stream.close() def _on_chunk_length(self, data): diff --git a/libs/tornado/stack_context.py b/libs/tornado/stack_context.py index c30a2598..8804d42d 100755 --- a/libs/tornado/stack_context.py +++ b/libs/tornado/stack_context.py @@ -14,20 +14,21 @@ # License for the specific language governing permissions and limitations # under the License. -"""StackContext allows applications to maintain threadlocal-like state +"""`StackContext` allows applications to maintain threadlocal-like state that follows execution as it moves to other execution contexts. The motivating examples are to eliminate the need for explicit -async_callback wrappers (as in tornado.web.RequestHandler), and to +``async_callback`` wrappers (as in `tornado.web.RequestHandler`), and to allow some additional context to be kept for logging. -This is slightly magic, but it's an extension of the idea that an exception -handler is a kind of stack-local state and when that stack is suspended -and resumed in a new context that state needs to be preserved. StackContext -shifts the burden of restoring that state from each call site (e.g. -wrapping each AsyncHTTPClient callback in async_callback) to the mechanisms -that transfer control from one context to another (e.g. AsyncHTTPClient -itself, IOLoop, thread pools, etc). +This is slightly magic, but it's an extension of the idea that an +exception handler is a kind of stack-local state and when that stack +is suspended and resumed in a new context that state needs to be +preserved. `StackContext` shifts the burden of restoring that state +from each call site (e.g. wrapping each `.AsyncHTTPClient` callback +in ``async_callback``) to the mechanisms that transfer control from +one context to another (e.g. `.AsyncHTTPClient` itself, `.IOLoop`, +thread pools, etc). Example usage:: @@ -52,7 +53,7 @@ Here are a few rules of thumb for when it's necessary: * If you're writing an asynchronous library that doesn't rely on a stack_context-aware library like `tornado.ioloop` or `tornado.iostream` (for example, if you're writing a thread pool), use - `stack_context.wrap()` before any asynchronous operations to capture the + `.stack_context.wrap()` before any asynchronous operations to capture the stack context from where the operation was started. * If you're writing an asynchronous library that has some shared @@ -68,9 +69,6 @@ Here are a few rules of thumb for when it's necessary: from __future__ import absolute_import, division, print_function, with_statement -import contextlib -import functools -import operator import sys import threading @@ -83,7 +81,7 @@ class StackContextInconsistentError(Exception): class _State(threading.local): def __init__(self): - self.contexts = () + self.contexts = (tuple(), None) _state = _State() @@ -107,34 +105,41 @@ class StackContext(object): context that are currently pending). This is an advanced feature and not necessary in most applications. """ - def __init__(self, context_factory, _active_cell=None): + def __init__(self, context_factory): self.context_factory = context_factory - self.active_cell = _active_cell or [True] + self.contexts = [] + + # StackContext protocol + def enter(self): + context = self.context_factory() + self.contexts.append(context) + context.__enter__() + + def exit(self, type, value, traceback): + context = self.contexts.pop() + context.__exit__(type, value, traceback) # Note that some of this code is duplicated in ExceptionStackContext # below. ExceptionStackContext is more common and doesn't need # the full generality of this class. def __enter__(self): self.old_contexts = _state.contexts - # _state.contexts is a tuple of (class, arg, active_cell) tuples - self.new_contexts = (self.old_contexts + - ((StackContext, self.context_factory, - self.active_cell),)) + self.new_contexts = (self.old_contexts[0] + (self,), self) _state.contexts = self.new_contexts + try: - self.context = self.context_factory() - self.context.__enter__() - except Exception: + self.enter() + except: _state.contexts = self.old_contexts raise - return lambda: operator.setitem(self.active_cell, 0, False) def __exit__(self, type, value, traceback): try: - return self.context.__exit__(type, value, traceback) + self.exit(type, value, traceback) finally: final_contexts = _state.contexts _state.contexts = self.old_contexts + # Generator coroutines and with-statements with non-local # effects interact badly. Check here for signs of # the stack getting out of sync. @@ -145,33 +150,32 @@ class StackContext(object): raise StackContextInconsistentError( 'stack_context inconsistency (may be caused by yield ' 'within a "with StackContext" block)') - self.old_contexts = self.new_contexts = None class ExceptionStackContext(object): """Specialization of StackContext for exception handling. - The supplied exception_handler function will be called in the + The supplied ``exception_handler`` function will be called in the event of an uncaught exception in this context. The semantics are similar to a try/finally clause, and intended use cases are to log an error, close a socket, or similar cleanup actions. The - exc_info triple (type, value, traceback) will be passed to the + ``exc_info`` triple ``(type, value, traceback)`` will be passed to the exception_handler function. If the exception handler returns true, the exception will be consumed and will not be propagated to other exception handlers. """ - def __init__(self, exception_handler, _active_cell=None): + def __init__(self, exception_handler): self.exception_handler = exception_handler - self.active_cell = _active_cell or [True] + + def exit(self, type, value, traceback): + if type is not None: + return self.exception_handler(type, value, traceback) def __enter__(self): self.old_contexts = _state.contexts - self.new_contexts = (self.old_contexts + - ((ExceptionStackContext, self.exception_handler, - self.active_cell),)) + self.new_contexts = (self.old_contexts[0], self) _state.contexts = self.new_contexts - return lambda: operator.setitem(self.active_cell, 0, False) def __exit__(self, type, value, traceback): try: @@ -180,98 +184,116 @@ class ExceptionStackContext(object): finally: final_contexts = _state.contexts _state.contexts = self.old_contexts + if final_contexts is not self.new_contexts: raise StackContextInconsistentError( 'stack_context inconsistency (may be caused by yield ' 'within a "with StackContext" block)') - self.old_contexts = self.new_contexts = None class NullContext(object): - """Resets the StackContext. + """Resets the `StackContext`. - Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient) - where the stack that caused the creating is not relevant to future - operations. + Useful when creating a shared resource on demand (e.g. an + `.AsyncHTTPClient`) where the stack that caused the creating is + not relevant to future operations. """ def __enter__(self): self.old_contexts = _state.contexts - _state.contexts = () + _state.contexts = (tuple(), None) def __exit__(self, type, value, traceback): _state.contexts = self.old_contexts -class _StackContextWrapper(functools.partial): - pass - - def wrap(fn): - """Returns a callable object that will restore the current StackContext + """Returns a callable object that will restore the current `StackContext` when executed. Use this whenever saving a callback to be executed later in a different execution context (either in a different thread or asynchronously in the same thread). """ - if fn is None or fn.__class__ is _StackContextWrapper: + # Check if function is already wrapped + if fn is None or hasattr(fn, '_wrapped'): return fn - # functools.wraps doesn't appear to work on functools.partial objects - #@functools.wraps(fn) + # Capture current stack head + contexts = _state.contexts + + #@functools.wraps def wrapped(*args, **kwargs): - callback, contexts, args = args[0], args[1], args[2:] + try: + # Force local state - switch to new stack chain + current_state = _state.contexts + _state.contexts = contexts - if _state.contexts: - new_contexts = [NullContext()] - else: - new_contexts = [] - if contexts: - new_contexts.extend(cls(arg, active_cell) - for (cls, arg, active_cell) in contexts - if active_cell[0]) - if len(new_contexts) > 1: - with _nested(*new_contexts): - callback(*args, **kwargs) - elif new_contexts: - with new_contexts[0]: - callback(*args, **kwargs) - else: - callback(*args, **kwargs) - return _StackContextWrapper(wrapped, fn, _state.contexts) + # Current exception + exc = (None, None, None) + top = None + + # Apply stack contexts + last_ctx = 0 + stack = contexts[0] + + # Apply state + for n in stack: + try: + n.enter() + last_ctx += 1 + except: + # Exception happened. Record exception info and store top-most handler + exc = sys.exc_info() + top = n.old_contexts[1] + + # Execute callback if no exception happened while restoring state + if top is None: + try: + fn(*args, **kwargs) + except: + exc = sys.exc_info() + top = contexts[1] + + # If there was exception, try to handle it by going through the exception chain + if top is not None: + exc = _handle_exception(top, exc) + else: + # Otherwise take shorter path and run stack contexts in reverse order + while last_ctx > 0: + last_ctx -= 1 + c = stack[last_ctx] + + try: + c.exit(*exc) + except: + exc = sys.exc_info() + top = c.old_contexts[1] + break + else: + top = None + + # If if exception happened while unrolling, take longer exception handler path + if top is not None: + exc = _handle_exception(top, exc) + + # If exception was not handled, raise it + if exc != (None, None, None): + raise_exc_info(exc) + finally: + _state.contexts = current_state + + wrapped._wrapped = True + return wrapped -@contextlib.contextmanager -def _nested(*managers): - """Support multiple context managers in a single with-statement. +def _handle_exception(tail, exc): + while tail is not None: + try: + if tail.exit(*exc): + exc = (None, None, None) + except: + exc = sys.exc_info() - Copied from the python 2.6 standard library. It's no longer present - in python 3 because the with statement natively supports multiple - context managers, but that doesn't help if the list of context - managers is not known until runtime. - """ - exits = [] - vars = [] - exc = (None, None, None) - try: - for mgr in managers: - exit = mgr.__exit__ - enter = mgr.__enter__ - vars.append(enter()) - exits.append(exit) - yield vars - except: - exc = sys.exc_info() - finally: - while exits: - exit = exits.pop() - try: - if exit(*exc): - exc = (None, None, None) - except: - exc = sys.exc_info() - if exc != (None, None, None): - # Don't rely on sys.exc_info() still containing - # the right information. Another exception may - # have been raised and caught by an exit method - raise_exc_info(exc) + tail = tail.old_contexts[1] + + return exc diff --git a/libs/tornado/tcpserver.py b/libs/tornado/tcpserver.py index 52ed70b1..fbd9c63d 100755 --- a/libs/tornado/tcpserver.py +++ b/libs/tornado/tcpserver.py @@ -28,13 +28,13 @@ from tornado.iostream import IOStream, SSLIOStream from tornado.netutil import bind_sockets, add_accept_handler, ssl_wrap_socket from tornado import process + class TCPServer(object): r"""A non-blocking, single-threaded TCP server. To use `TCPServer`, define a subclass which overrides the `handle_stream` method. - `TCPServer` can serve SSL traffic with Python 2.6+ and OpenSSL. To make this server serve SSL traffic, send the ssl_options dictionary argument with the arguments required for the `ssl.wrap_socket` method, including "certfile" and "keyfile":: @@ -59,9 +59,9 @@ class TCPServer(object): server.start(0) # Forks multiple sub-processes IOLoop.instance().start() - When using this interface, an `IOLoop` must *not* be passed + When using this interface, an `.IOLoop` must *not* be passed to the `TCPServer` constructor. `start` will always start - the server on the default singleton `IOLoop`. + the server on the default singleton `.IOLoop`. 3. `add_sockets`: advanced multi-process:: @@ -76,7 +76,7 @@ class TCPServer(object): flexibility in when the fork happens. `add_sockets` can also be used in single-process servers if you want to create your listening sockets in some way other than - `bind_sockets`. + `~tornado.netutil.bind_sockets`. """ def __init__(self, io_loop=None, ssl_options=None): self.io_loop = io_loop @@ -108,7 +108,7 @@ class TCPServer(object): This method may be called more than once to listen on multiple ports. `listen` takes effect immediately; it is not necessary to call `TCPServer.start` afterwards. It is, however, necessary to start - the `IOLoop`. + the `.IOLoop`. """ sockets = bind_sockets(port, address=address) self.add_sockets(sockets) @@ -117,13 +117,13 @@ class TCPServer(object): """Makes this server start accepting connections on the given sockets. The ``sockets`` parameter is a list of socket objects such as - those returned by `bind_sockets`. + those returned by `~tornado.netutil.bind_sockets`. `add_sockets` is typically used in combination with that method and `tornado.process.fork_processes` to provide greater control over the initialization of a multi-process server. """ if self.io_loop is None: - self.io_loop = IOLoop.instance() + self.io_loop = IOLoop.current() for sock in sockets: self._sockets[sock.fileno()] = sock @@ -144,12 +144,12 @@ class TCPServer(object): Address may be either an IP address or hostname. If it's a hostname, the server will listen on all IP addresses associated with the name. Address may be an empty string or None to listen on all - available interfaces. Family may be set to either ``socket.AF_INET`` - or ``socket.AF_INET6`` to restrict to ipv4 or ipv6 addresses, otherwise + available interfaces. Family may be set to either `socket.AF_INET` + or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise both will be used if available. The ``backlog`` argument has the same meaning as for - `socket.listen`. + `socket.listen `. This method may be called multiple times prior to `start` to listen on multiple ports or interfaces. @@ -162,7 +162,7 @@ class TCPServer(object): self._pending_sockets.extend(sockets) def start(self, num_processes=1): - """Starts this server in the IOLoop. + """Starts this server in the `.IOLoop`. By default, we run the server in this process and do not fork any additional child process. @@ -199,7 +199,7 @@ class TCPServer(object): sock.close() def handle_stream(self, stream, address): - """Override to handle a new `IOStream` from an incoming connection.""" + """Override to handle a new `.IOStream` from an incoming connection.""" raise NotImplementedError() def _handle_connection(self, connection, address): diff --git a/libs/tornado/template.py b/libs/tornado/template.py index 4f61de3f..8e1bfbae 100755 --- a/libs/tornado/template.py +++ b/libs/tornado/template.py @@ -79,9 +79,10 @@ We provide the functions escape(), url_escape(), json_encode(), and squeeze() to all templates by default. Typical applications do not create `Template` or `Loader` instances by -hand, but instead use the `render` and `render_string` methods of +hand, but instead use the `~.RequestHandler.render` and +`~.RequestHandler.render_string` methods of `tornado.web.RequestHandler`, which load templates automatically based -on the ``template_path`` `Application` setting. +on the ``template_path`` `.Application` setting. Syntax Reference ---------------- @@ -109,7 +110,7 @@ with ``{# ... #}``. ``{% autoescape *function* %}`` Sets the autoescape mode for the current file. This does not affect other files, even those referenced by ``{% include %}``. Note that - autoescaping can also be configured globally, at the `Application` + autoescaping can also be configured globally, at the `.Application` or `Loader`.:: {% autoescape xhtml_escape %} @@ -298,14 +299,14 @@ class Template(object): class BaseLoader(object): - """Base class for template loaders.""" + """Base class for template loaders. + + You must use a template loader to use template constructs like + ``{% extends %}`` and ``{% include %}``. The loader caches all + templates after they are loaded the first time. + """ def __init__(self, autoescape=_DEFAULT_AUTOESCAPE, namespace=None): - """Creates a template loader. - - root_directory may be the empty string if this loader does not - use the filesystem. - - autoescape must be either None or a string naming a function + """``autoescape`` must be either None or a string naming a function in the template namespace, such as "xhtml_escape". """ self.autoescape = autoescape @@ -341,10 +342,6 @@ class BaseLoader(object): class Loader(BaseLoader): """A template loader that loads from a single root directory. - - You must use a template loader to use template constructs like - {% extends %} and {% include %}. Loader caches all templates after - they are loaded the first time. """ def __init__(self, root_directory, **kwargs): super(Loader, self).__init__(**kwargs) diff --git a/libs/tornado/testing.py b/libs/tornado/testing.py index 30fe4e29..51663a4a 100755 --- a/libs/tornado/testing.py +++ b/libs/tornado/testing.py @@ -1,21 +1,13 @@ #!/usr/bin/env python """Support classes for automated testing. -This module contains three parts: +* `AsyncTestCase` and `AsyncHTTPTestCase`: Subclasses of unittest.TestCase + with additional support for testing asynchronous (`.IOLoop` based) code. -* `AsyncTestCase`/`AsyncHTTPTestCase`: Subclasses of unittest.TestCase - with additional support for testing asynchronous (IOLoop-based) code. - -* `LogTrapTestCase`: Subclass of unittest.TestCase that discards log output - from tests that pass and only produces output for failing tests. +* `ExpectLog` and `LogTrapTestCase`: Make test logs less spammy. * `main()`: A simple test runner (wrapper around unittest.main()) with support for the tornado.autoreload module to rerun the tests when code changes. - -These components may be used together or independently. In particular, -it is safe to combine AsyncTestCase and LogTrapTestCase via multiple -inheritance. See the docstrings for each class/function below for more -information. """ from __future__ import absolute_import, division, print_function, with_statement @@ -46,12 +38,11 @@ import re import signal import socket import sys -import types try: - from io import StringIO # py3 -except ImportError: from cStringIO import StringIO # py2 +except ImportError: + from io import StringIO # py3 # Tornado's own test suite requires the updated unittest module # (either py27+ or unittest2) so tornado.test.util enforces @@ -91,30 +82,53 @@ def bind_unused_port(): return sock, port +def get_async_test_timeout(): + """Get the global timeout setting for async tests. + + Returns a float, the timeout in seconds. + """ + try: + return float(os.environ.get('ASYNC_TEST_TIMEOUT')) + except (ValueError, TypeError): + return 5 + + class AsyncTestCase(unittest.TestCase): - """TestCase subclass for testing IOLoop-based asynchronous code. + """`~unittest.TestCase` subclass for testing `.IOLoop`-based + asynchronous code. - The unittest framework is synchronous, so the test must be complete - by the time the test method returns. This method provides the stop() - and wait() methods for this purpose. The test method itself must call - self.wait(), and asynchronous callbacks should call self.stop() to signal - completion. + The unittest framework is synchronous, so the test must be + complete by the time the test method returns. This class provides + the `stop()` and `wait()` methods for this purpose. The test + method itself must call ``self.wait()``, and asynchronous + callbacks should call ``self.stop()`` to signal completion. + Alternately, the `gen_test` decorator can be used to use yield points + from the `tornado.gen` module. - By default, a new IOLoop is constructed for each test and is available - as self.io_loop. This IOLoop should be used in the construction of + By default, a new `.IOLoop` is constructed for each test and is available + as ``self.io_loop``. This `.IOLoop` should be used in the construction of HTTP clients/servers, etc. If the code being tested requires a - global IOLoop, subclasses should override get_new_ioloop to return it. + global `.IOLoop`, subclasses should override `get_new_ioloop` to return it. - The IOLoop's start and stop methods should not be called directly. - Instead, use self.stop self.wait. Arguments passed to self.stop are - returned from self.wait. It is possible to have multiple - wait/stop cycles in the same test. + The `.IOLoop`'s ``start`` and ``stop`` methods should not be + called directly. Instead, use `self.stop ` and `self.wait + `. Arguments passed to ``self.stop`` are returned from + ``self.wait``. It is possible to have multiple ``wait``/``stop`` + cycles in the same test. Example:: - # This test uses an asynchronous style similar to most async - # application code. + # This test uses argument passing between self.stop and self.wait. class MyTestCase(AsyncTestCase): + def test_http_fetch(self): + client = AsyncHTTPClient(self.io_loop) + client.fetch("http://www.tornadoweb.org/", self.stop) + response = self.wait() + # Test contents of response + self.assertIn("FriendFeed", response.body) + + # This test uses an explicit callback-based style. + class MyTestCase2(AsyncTestCase): def test_http_fetch(self): client = AsyncHTTPClient(self.io_loop) client.fetch("http://www.tornadoweb.org/", self.handle_fetch) @@ -128,19 +142,6 @@ class AsyncTestCase(unittest.TestCase): # self.wait() in test_http_fetch() via stack_context. self.assertIn("FriendFeed", response.body) self.stop() - - # This test uses the argument passing between self.stop and self.wait - # for a simpler, more synchronous style. - # This style is recommended over the preceding example because it - # keeps the assertions in the test method itself, and is therefore - # less sensitive to the subtleties of stack_context. - class MyTestCase2(AsyncTestCase): - def test_http_fetch(self): - client = AsyncHTTPClient(self.io_loop) - client.fetch("http://www.tornadoweb.org/", self.stop) - response = self.wait() - # Test contents of response - self.assertIn("FriendFeed", response.body) """ def __init__(self, *args, **kwargs): super(AsyncTestCase, self).__init__(*args, **kwargs) @@ -165,16 +166,21 @@ class AsyncTestCase(unittest.TestCase): # set FD_CLOEXEC on its file descriptors) self.io_loop.close(all_fds=True) super(AsyncTestCase, self).tearDown() + # In case an exception escaped or the StackContext caught an exception + # when there wasn't a wait() to re-raise it, do so here. + # This is our last chance to raise an exception in a way that the + # unittest machinery understands. + self.__rethrow() def get_new_ioloop(self): - """Creates a new IOLoop for this test. May be overridden in - subclasses for tests that require a specific IOLoop (usually - the singleton). + """Creates a new `.IOLoop` for this test. May be overridden in + subclasses for tests that require a specific `.IOLoop` (usually + the singleton `.IOLoop.instance()`). """ return IOLoop() def _handle_exception(self, typ, value, tb): - self.__failure = sys.exc_info() + self.__failure = (typ, value, tb) self.stop() return True @@ -187,16 +193,18 @@ class AsyncTestCase(unittest.TestCase): def run(self, result=None): with ExceptionStackContext(self._handle_exception): super(AsyncTestCase, self).run(result) - # In case an exception escaped super.run or the StackContext caught - # an exception when there wasn't a wait() to re-raise it, do so here. + # As a last resort, if an exception escaped super.run() and wasn't + # re-raised in tearDown, raise it here. This will cause the + # unittest run to fail messily, but that's better than silently + # ignoring an error. self.__rethrow() def stop(self, _arg=None, **kwargs): - """Stops the ioloop, causing one pending (or future) call to wait() + """Stops the `.IOLoop`, causing one pending (or future) call to `wait()` to return. - Keyword arguments or a single positional argument passed to stop() are - saved and will be returned by wait(). + Keyword arguments or a single positional argument passed to `stop()` are + saved and will be returned by `wait()`. """ assert _arg is None or not kwargs self.__stop_args = kwargs or _arg @@ -205,14 +213,19 @@ class AsyncTestCase(unittest.TestCase): self.__running = False self.__stopped = True - def wait(self, condition=None, timeout=5): - """Runs the IOLoop until stop is called or timeout has passed. + def wait(self, condition=None, timeout=None): + """Runs the `.IOLoop` until stop is called or timeout has passed. - In the event of a timeout, an exception will be thrown. + In the event of a timeout, an exception will be thrown. The default + timeout is 5 seconds; it may be overridden with a ``timeout`` keyword + argument or globally with the ASYNC_TEST_TIMEOUT environment variable. - If condition is not None, the IOLoop will be restarted after stop() - until condition() returns true. + If ``condition`` is not None, the `.IOLoop` will be restarted + after `stop()` until ``condition()`` returns true. """ + if timeout is None: + timeout = get_async_test_timeout() + if not self.__stopped: if timeout: def timeout_func(): @@ -244,9 +257,9 @@ class AsyncTestCase(unittest.TestCase): class AsyncHTTPTestCase(AsyncTestCase): """A test case that starts up an HTTP server. - Subclasses must override get_app(), which returns the - tornado.web.Application (or other HTTPServer callback) to be tested. - Tests will typically use the provided self.http_client to fetch + Subclasses must override `get_app()`, which returns the + `tornado.web.Application` (or other `.HTTPServer` callback) to be tested. + Tests will typically use the provided ``self.http_client`` to fetch URLs from this server. Example:: @@ -283,17 +296,17 @@ class AsyncHTTPTestCase(AsyncTestCase): def get_app(self): """Should be overridden by subclasses to return a - tornado.web.Application or other HTTPServer callback. + `tornado.web.Application` or other `.HTTPServer` callback. """ raise NotImplementedError() def fetch(self, path, **kwargs): """Convenience method to synchronously fetch a url. - The given path will be appended to the local server's host and port. - Any additional kwargs will be passed directly to - AsyncHTTPClient.fetch (and so could be used to pass method="POST", - body="...", etc). + The given path will be appended to the local server's host and + port. Any additional kwargs will be passed directly to + `.AsyncHTTPClient.fetch` (and so could be used to pass + ``method="POST"``, ``body="..."``, etc). """ self.http_client.fetch(self.get_url(path), self.stop, **kwargs) return self.wait() @@ -357,34 +370,58 @@ class AsyncHTTPSTestCase(AsyncHTTPTestCase): return 'https' -def gen_test(f): - """Testing equivalent of ``@gen.engine``, to be applied to test methods. +def gen_test(func=None, timeout=None): + """Testing equivalent of ``@gen.coroutine``, to be applied to test methods. - ``@gen.engine`` cannot be used on tests because the `IOLoop` is not + ``@gen.coroutine`` cannot be used on tests because the `.IOLoop` is not already running. ``@gen_test`` should be applied to test methods on subclasses of `AsyncTestCase`. - Note that unlike most uses of ``@gen.engine``, ``@gen_test`` can - detect automatically when the function finishes cleanly so there - is no need to run a callback to signal completion. - Example:: + class MyTest(AsyncHTTPTestCase): @gen_test def test_something(self): response = yield gen.Task(self.fetch('/')) + By default, ``@gen_test`` times out after 5 seconds. The timeout may be + overridden globally with the ASYNC_TEST_TIMEOUT environment variable, + or for each test with the ``timeout`` keyword argument:: + + class MyTest(AsyncHTTPTestCase): + @gen_test(timeout=10) + def test_something_slow(self): + response = yield gen.Task(self.fetch('/')) + + If both the environment variable and the parameter are set, ``gen_test`` + uses the maximum of the two. """ - @functools.wraps(f) - def wrapper(self, *args, **kwargs): - result = f(self, *args, **kwargs) - if result is None: - return - assert isinstance(result, types.GeneratorType) - runner = gen.Runner(result, self.stop) - runner.run() - self.wait() - return wrapper + if timeout is None: + timeout = get_async_test_timeout() + + def wrap(f): + f = gen.coroutine(f) + + @functools.wraps(f) + def wrapper(self): + return self.io_loop.run_sync( + functools.partial(f, self), timeout=timeout) + return wrapper + + if func is not None: + # Used like: + # @gen_test + # def f(self): + # pass + return wrap(func) + else: + # Used like @gen_test(timeout=10) + return wrap + + +# Without this attribute, nosetests will try to run gen_test as a test +# anywhere it is imported. +gen_test.__test__ = False class LogTrapTestCase(unittest.TestCase): @@ -395,13 +432,13 @@ class LogTrapTestCase(unittest.TestCase): the test succeeds, so this class can be useful to minimize the noise. Simply use it as a base class for your test case. It is safe to combine with AsyncTestCase via multiple inheritance - ("class MyTestCase(AsyncHTTPTestCase, LogTrapTestCase):") + (``class MyTestCase(AsyncHTTPTestCase, LogTrapTestCase):``) - This class assumes that only one log handler is configured and that - it is a StreamHandler. This is true for both logging.basicConfig - and the "pretty logging" configured by tornado.options. It is not - compatible with other log buffering mechanisms, such as those provided - by some test runners. + This class assumes that only one log handler is configured and + that it is a `~logging.StreamHandler`. This is true for both + `logging.basicConfig` and the "pretty logging" configured by + `tornado.options`. It is not compatible with other log buffering + mechanisms, such as those provided by some test runners. """ def run(self, result=None): logger = logging.getLogger() @@ -409,7 +446,7 @@ class LogTrapTestCase(unittest.TestCase): logging.basicConfig() handler = logger.handlers[0] if (len(logger.handlers) > 1 or - not isinstance(handler, logging.StreamHandler)): + not isinstance(handler, logging.StreamHandler)): # Logging has been configured in a way we don't recognize, # so just leave it alone. super(LogTrapTestCase, self).run(result) @@ -486,10 +523,11 @@ def main(**kwargs): be specified. Projects with many tests may wish to define a test script like - tornado/test/runtests.py. This script should define a method all() - which returns a test suite and then call tornado.testing.main(). - Note that even when a test script is used, the all() test suite may - be overridden by naming a single test on the command line:: + ``tornado/test/runtests.py``. This script should define a method + ``all()`` which returns a test suite and then call + `tornado.testing.main()`. Note that even when a test script is + used, the ``all()`` test suite may be overridden by naming a + single test on the command line:: # Runs all tests python -m tornado.test.runtests diff --git a/libs/tornado/util.py b/libs/tornado/util.py index 69de2c8e..a2fba779 100755 --- a/libs/tornado/util.py +++ b/libs/tornado/util.py @@ -1,4 +1,14 @@ -"""Miscellaneous utility functions.""" +"""Miscellaneous utility functions and classes. + +This module is used internally by Tornado. It is not necessarily expected +that the functions and classes defined here will be useful to other +applications, but they are documented here in case they are. + +The one public-facing part of this module is the `Configurable` class +and its `~Configurable.configure` method, which becomes a part of the +interface of its subclasses, including `.AsyncHTTPClient`, `.IOLoop`, +and `.Resolver`. +""" from __future__ import absolute_import, division, print_function, with_statement @@ -8,7 +18,8 @@ import zlib class ObjectDict(dict): - """Makes a dictionary behave like an object.""" + """Makes a dictionary behave like an object, with attribute-style access. + """ def __getattr__(self, name): try: return self[name] @@ -52,6 +63,7 @@ class GzipDecompressor(object): def import_object(name): """Imports an object by name. + import_object('x') is equivalent to 'import x'. import_object('x.y.z') is equivalent to 'from x.y import z'. >>> import tornado.escape @@ -59,10 +71,23 @@ def import_object(name): True >>> import_object('tornado.escape.utf8') is tornado.escape.utf8 True + >>> import_object('tornado') is tornado + True + >>> import_object('tornado.missing_module') + Traceback (most recent call last): + ... + ImportError: No module named missing_module """ + if name.count('.') == 0: + return __import__(name, None, None) + parts = name.split('.') obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0) - return getattr(obj, parts[-1]) + try: + return getattr(obj, parts[-1]) + except AttributeError: + raise ImportError("No module named %s" % parts[-1]) + # Fake unicode literal support: Python 3.2 doesn't have the u'' marker for # literal strings, and alternative solutions like "from __future__ import @@ -115,16 +140,17 @@ class Configurable(object): The implementation subclass as well as optional keyword arguments to its initializer can be set globally at runtime with `configure`. - By using the constructor as the factory method, the interface looks like - a normal class, ``isinstance()`` works as usual, etc. This pattern - is most useful when the choice of implementation is likely to be a - global decision (e.g. when epoll is available, always use it instead of - select), or when a previously-monolithic class has been split into - specialized subclasses. + By using the constructor as the factory method, the interface + looks like a normal class, `isinstance` works as usual, etc. This + pattern is most useful when the choice of implementation is likely + to be a global decision (e.g. when `~select.epoll` is available, + always use it instead of `~select.select`), or when a + previously-monolithic class has been split into specialized + subclasses. Configurable subclasses must define the class methods `configurable_base` and `configurable_default`, and use the instance - method `initialize` instead of `__init__`. + method `initialize` instead of ``__init__``. """ __impl_class = None __impl_kwargs = None @@ -163,7 +189,7 @@ class Configurable(object): def initialize(self): """Initialize a `Configurable` subclass instance. - Configurable classes should use `initialize` instead of `__init__`. + Configurable classes should use `initialize` instead of ``__init__``. """ @classmethod @@ -210,8 +236,6 @@ class ArgReplacer(object): and similar wrappers. """ def __init__(self, func, name): - """Create an ArgReplacer for the named argument to the given function. - """ self.name = name try: self.arg_pos = inspect.getargspec(func).args.index(self.name) diff --git a/libs/tornado/web.py b/libs/tornado/web.py index 3cccba56..6abf42c0 100755 --- a/libs/tornado/web.py +++ b/libs/tornado/web.py @@ -14,13 +14,12 @@ # License for the specific language governing permissions and limitations # under the License. -""" -The Tornado web framework looks a bit like web.py (http://webpy.org/) or -Google's webapp (http://code.google.com/appengine/docs/python/tools/webapp/), -but with additional tools and optimizations to take advantage of the -Tornado non-blocking web server and tools. +"""``tornado.web`` provides a simple web framework with asynchronous +features that allow it to scale to large numbers of open connections, +making it ideal for `long polling +`_. -Here is the canonical "Hello, world" example app:: +Here is a simple "Hello, world" example app:: import tornado.ioloop import tornado.web @@ -36,17 +35,19 @@ Here is the canonical "Hello, world" example app:: application.listen(8888) tornado.ioloop.IOLoop.instance().start() -See the Tornado walkthrough on http://tornadoweb.org for more details -and a good getting started guide. +See the :doc:`Tornado overview ` for more details and a good getting +started guide. Thread-safety notes ------------------- -In general, methods on RequestHandler and elsewhere in tornado are not -thread-safe. In particular, methods such as write(), finish(), and -flush() must only be called from the main thread. If you use multiple -threads it is important to use IOLoop.add_callback to transfer control -back to the main thread before finishing the request. +In general, methods on `RequestHandler` and elsewhere in Tornado are +not thread-safe. In particular, methods such as +`~RequestHandler.write()`, `~RequestHandler.finish()`, and +`~RequestHandler.flush()` must only be called from the main thread. If +you use multiple threads it is important to use `.IOLoop.add_callback` +to transfer control back to the main thread before finishing the +request. """ from __future__ import absolute_import, division, print_function, with_statement @@ -72,6 +73,7 @@ import traceback import types import uuid +from tornado.concurrent import Future from tornado import escape from tornado import httputil from tornado import locale @@ -103,11 +105,11 @@ except ImportError: class RequestHandler(object): - """Subclass this class and define get() or post() to make a handler. + """Subclass this class and define `get()` or `post()` to make a handler. If you want to support more methods than the standard GET/HEAD/POST, you - should override the class variable SUPPORTED_METHODS in your - RequestHandler class. + should override the class variable ``SUPPORTED_METHODS`` in your + `RequestHandler` subclass. """ SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PATCH", "PUT", "OPTIONS") @@ -167,7 +169,7 @@ class RequestHandler(object): @property def settings(self): - """An alias for `self.application.settings`.""" + """An alias for `self.application.settings `.""" return self.application.settings def head(self, *args, **kwargs): @@ -192,7 +194,7 @@ class RequestHandler(object): raise HTTPError(405) def prepare(self): - """Called at the beginning of a request before `get`/`post`/etc. + """Called at the beginning of a request before `get`/`post`/etc. Override this method to perform common initialization regardless of the request method. @@ -228,10 +230,10 @@ class RequestHandler(object): def clear(self): """Resets all headers and content for this response.""" self._headers = httputil.HTTPHeaders({ - "Server": "TornadoServer/%s" % tornado.version, - "Content-Type": "text/html; charset=UTF-8", - "Date": httputil.format_timestamp(time.gmtime()), - }) + "Server": "TornadoServer/%s" % tornado.version, + "Content-Type": "text/html; charset=UTF-8", + "Date": httputil.format_timestamp(time.gmtime()), + }) self.set_default_headers() if not self.request.supports_http_1_1(): if self.request.headers.get("Connection") == "Keep-Alive": @@ -253,10 +255,11 @@ class RequestHandler(object): def set_status(self, status_code, reason=None): """Sets the status code for our response. - :arg int status_code: Response status code. If `reason` is ``None``, - it must be present in `httplib.responses`. + :arg int status_code: Response status code. If ``reason`` is ``None``, + it must be present in `httplib.responses `. :arg string reason: Human-readable reason phrase describing the status - code. If ``None``, it will be filled in from `httplib.responses`. + code. If ``None``, it will be filled in from + `httplib.responses `. """ self._status_code = status_code if reason is not None: @@ -363,8 +366,8 @@ class RequestHandler(object): By default, this method decodes the argument as utf-8 and returns a unicode string, but this may be overridden in subclasses. - This method is used as a filter for both get_argument() and for - values extracted from the url and passed to get()/post()/etc. + This method is used as a filter for both `get_argument()` and for + values extracted from the url and passed to `get()`/`post()`/etc. The name of the argument is provided if known, but may be None (e.g. for unnamed groups in the url regex). @@ -373,6 +376,7 @@ class RequestHandler(object): @property def cookies(self): + """An alias for `self.request.cookies <.httpserver.HTTPRequest.cookies>`.""" return self.request.cookies def get_cookie(self, name, default=None): @@ -496,8 +500,8 @@ class RequestHandler(object): To write the output to the network, use the flush() method below. If the given chunk is a dictionary, we write it as JSON and set - the Content-Type of the response to be application/json. - (if you want to send JSON as a different Content-Type, call + the Content-Type of the response to be ``application/json``. + (if you want to send JSON as a different ``Content-Type``, call set_header *after* calling write()). Note that lists are not converted to JSON because of a potential @@ -604,8 +608,8 @@ class RequestHandler(object): def render_string(self, template_name, **kwargs): """Generate the given template with the given arguments. - We return the generated string. To generate and write a template - as a response, use render() above. + We return the generated byte string (in utf8). To generate and + write a template as a response, use render() above. """ # If no template_path is specified, use the path of the calling file template_path = self.get_template_path() @@ -743,6 +747,9 @@ class RequestHandler(object): self._log() self._finished = True self.on_finish() + # Break up a reference cycle between this handler and the + # _ui_module closures to allow for faster GC on CPython. + self.ui = None def send_error(self, status_code=500, **kwargs): """Sends the given HTTP error code to the browser. @@ -823,9 +830,9 @@ class RequestHandler(object): def locale(self): """The local for the current session. - Determined by either get_user_locale, which you can override to + Determined by either `get_user_locale`, which you can override to set the locale based on, e.g., a user preference stored in a - database, or get_browser_locale, which uses the Accept-Language + database, or `get_browser_locale`, which uses the ``Accept-Language`` header. """ if not hasattr(self, "_locale"): @@ -838,15 +845,15 @@ class RequestHandler(object): def get_user_locale(self): """Override to determine the locale from the authenticated user. - If None is returned, we fall back to get_browser_locale(). + If None is returned, we fall back to `get_browser_locale()`. - This method should return a tornado.locale.Locale object, - most likely obtained via a call like tornado.locale.get("en") + This method should return a `tornado.locale.Locale` object, + most likely obtained via a call like ``tornado.locale.get("en")`` """ return None def get_browser_locale(self, default="en_US"): - """Determines the user's locale from Accept-Language header. + """Determines the user's locale from ``Accept-Language`` header. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 """ @@ -873,9 +880,9 @@ class RequestHandler(object): def current_user(self): """The authenticated user for this request. - Determined by either get_current_user, which you can override to - set the user based on, e.g., a cookie. If that method is not - overridden, this method always returns None. + This is a cached version of `get_current_user`, which you can + override to set the user based on, e.g., a cookie. If that + method is not overridden, this method always returns None. We lazy-load the current user the first time this method is called and cache the result after that. @@ -891,7 +898,7 @@ class RequestHandler(object): def get_login_url(self): """Override to customize the login URL based on the request. - By default, we use the 'login_url' application setting. + By default, we use the ``login_url`` application setting. """ self.require_setting("login_url", "@tornado.web.authenticated") return self.application.settings["login_url"] @@ -899,7 +906,7 @@ class RequestHandler(object): def get_template_path(self): """Override to customize template path for each handler. - By default, we use the 'template_path' application setting. + By default, we use the ``template_path`` application setting. Return None to load templates relative to the calling file. """ return self.application.settings.get("template_path") @@ -925,21 +932,21 @@ class RequestHandler(object): return self._xsrf_token def check_xsrf_cookie(self): - """Verifies that the '_xsrf' cookie matches the '_xsrf' argument. + """Verifies that the ``_xsrf`` cookie matches the ``_xsrf`` argument. - To prevent cross-site request forgery, we set an '_xsrf' + To prevent cross-site request forgery, we set an ``_xsrf`` cookie and include the same value as a non-cookie - field with all POST requests. If the two do not match, we + field with all ``POST`` requests. If the two do not match, we reject the form submission as a potential forgery. - The _xsrf value may be set as either a form field named _xsrf - or in a custom HTTP header named X-XSRFToken or X-CSRFToken + The ``_xsrf`` value may be set as either a form field named ``_xsrf`` + or in a custom HTTP header named ``X-XSRFToken`` or ``X-CSRFToken`` (the latter is accepted for compatibility with Django). See http://en.wikipedia.org/wiki/Cross-site_request_forgery Prior to release 1.1.1, this check was ignored if the HTTP header - "X-Requested-With: XMLHTTPRequest" was present. This exception + ``X-Requested-With: XMLHTTPRequest`` was present. This exception has been shown to be insecure and has been removed. For more information please see http://www.djangoproject.com/weblog/2011/feb/08/security/ @@ -954,14 +961,17 @@ class RequestHandler(object): raise HTTPError(403, "XSRF cookie does not match POST argument") def xsrf_form_html(self): - """An HTML element to be included with all POST forms. + """An HTML ```` element to be included with all POST forms. - It defines the _xsrf input value, which we check on all POST + It defines the ``_xsrf`` input value, which we check on all POST requests to prevent cross-site request forgery. If you have set - the 'xsrf_cookies' application setting, you must include this + the ``xsrf_cookies`` application setting, you must include this HTML within all of your HTML forms. - See check_xsrf_cookie() above for more information. + In a template, this method should be called with ``{% module + xsrf_form_html() %}`` + + See `check_xsrf_cookie()` above for more information. """ return '' @@ -969,11 +979,11 @@ class RequestHandler(object): def static_url(self, path, include_host=None): """Returns a static URL for the given relative static file path. - This method requires you set the 'static_path' setting in your + This method requires you set the ``static_path`` setting in your application (which specifies the root directory of your static files). - We append ?v= to the returned URL, which makes our + We append ``?v=`` to the returned URL, which makes our static file handler set an infinite expiration header on the returned content. The signature is based on the content of the file. @@ -1142,10 +1152,18 @@ class RequestHandler(object): def asynchronous(method): """Wrap request handler methods with this if they are asynchronous. + This decorator should only be applied to the :ref:`HTTP verb + methods `; its behavior is undefined for any other method. + This decorator does not *make* a method asynchronous; it tells + the framework that the method *is* asynchronous. For this decorator + to be useful the method must (at least sometimes) do something + asynchronous. + If this decorator is given, the response is not finished when the - method returns. It is up to the request handler to call self.finish() - to finish the HTTP request. Without this decorator, the request is - automatically finished when the get() or post() method returns. :: + method returns. It is up to the request handler to call + `self.finish() ` to finish the HTTP + request. Without this decorator, the request is automatically + finished when the ``get()`` or ``post()`` method returns. Example:: class MyRequestHandler(web.RequestHandler): @web.asynchronous @@ -1158,6 +1176,8 @@ def asynchronous(method): self.finish() """ + # Delay the IOLoop import because it's not available on app engine. + from tornado.ioloop import IOLoop @functools.wraps(method) def wrapper(self, *args, **kwargs): if self.application._wsgi: @@ -1165,14 +1185,27 @@ def asynchronous(method): self._auto_finish = False with stack_context.ExceptionStackContext( self._stack_context_handle_exception): - return method(self, *args, **kwargs) + result = method(self, *args, **kwargs) + if isinstance(result, Future): + # If @asynchronous is used with @gen.coroutine, (but + # not @gen.engine), we can automatically finish the + # request when the future resolves. Additionally, + # the Future will swallow any exceptions so we need + # to throw them back out to the stack context to finish + # the request. + def future_complete(f): + f.result() + if not self._finished: + self.finish() + IOLoop.current().add_future(result, future_complete) + return result return wrapper def removeslash(method): """Use this decorator to remove trailing slashes from the request path. - For example, a request to ``'/foo/'`` would redirect to ``'/foo'`` with this + For example, a request to ``/foo/`` would redirect to ``/foo`` with this decorator. Your request handler mapping should use a regular expression like ``r'/foo/*'`` in conjunction with using the decorator. """ @@ -1195,9 +1228,9 @@ def removeslash(method): def addslash(method): """Use this decorator to add a missing trailing slash to the request path. - For example, a request to '/foo' would redirect to '/foo/' with this + For example, a request to ``/foo`` would redirect to ``/foo/`` with this decorator. Your request handler mapping should use a regular expression - like r'/foo/?' in conjunction with using the decorator. + like ``r'/foo/?'`` in conjunction with using the decorator. """ @functools.wraps(method) def wrapper(self, *args, **kwargs): @@ -1226,35 +1259,36 @@ class Application(object): http_server.listen(8080) ioloop.IOLoop.instance().start() - The constructor for this class takes in a list of URLSpec objects + The constructor for this class takes in a list of `URLSpec` objects or (regexp, request_class) tuples. When we receive requests, we iterate over the list in order and instantiate an instance of the first request class whose regexp matches the request path. - Each tuple can contain an optional third element, which should be a - dictionary if it is present. That dictionary is passed as keyword - arguments to the contructor of the handler. This pattern is used - for the StaticFileHandler below (note that a StaticFileHandler - can be installed automatically with the static_path setting described - below):: + Each tuple can contain an optional third element, which should be + a dictionary if it is present. That dictionary is passed as + keyword arguments to the contructor of the handler. This pattern + is used for the `StaticFileHandler` in this example (note that a + `StaticFileHandler` can be installed automatically with the + static_path setting described below):: application = web.Application([ (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}), ]) - We support virtual hosts with the add_handlers method, which takes in + We support virtual hosts with the `add_handlers` method, which takes in a host regular expression as the first argument:: application.add_handlers(r"www\.myhost\.com", [ (r"/article/([0-9]+)", ArticleHandler), ]) - You can serve static files by sending the static_path setting as a - keyword argument. We will serve those files from the /static/ URI - (this is configurable with the static_url_prefix setting), - and we will serve /favicon.ico and /robots.txt from the same directory. - A custom subclass of StaticFileHandler can be specified with the - static_handler_class setting. + You can serve static files by sending the ``static_path`` setting + as a keyword argument. We will serve those files from the + ``/static/`` URI (this is configurable with the + ``static_url_prefix`` setting), and we will serve ``/favicon.ico`` + and ``/robots.txt`` from the same directory. A custom subclass of + `StaticFileHandler` can be specified with the + ``static_handler_class`` setting. """ def __init__(self, handlers=None, default_host="", transforms=None, wsgi=False, **settings): @@ -1301,15 +1335,16 @@ class Application(object): def listen(self, port, address="", **kwargs): """Starts an HTTP server for this application on the given port. - This is a convenience alias for creating an HTTPServer object - and calling its listen method. Keyword arguments not - supported by HTTPServer.listen are passed to the HTTPServer - constructor. For advanced uses (e.g. preforking), do not use - this method; create an HTTPServer and call its bind/start - methods directly. + This is a convenience alias for creating an `.HTTPServer` + object and calling its listen method. Keyword arguments not + supported by `HTTPServer.listen <.TCPServer.listen>` are passed to the + `.HTTPServer` constructor. For advanced uses + (e.g. multi-process mode), do not use this method; create an + `.HTTPServer` and call its + `.TCPServer.bind`/`.TCPServer.start` methods directly. Note that after calling this method you still need to call - IOLoop.instance().start() to start the server. + ``IOLoop.instance().start()`` to start the server. """ # import is here rather than top level because HTTPServer # is not importable on appengine @@ -1337,7 +1372,7 @@ class Application(object): self.handlers.append((re.compile(host_pattern), handlers)) for spec in host_handlers: - if isinstance(spec, type(())): + if isinstance(spec, (tuple, list)): assert len(spec) in (2, 3) pattern = spec[0] handler = spec[1] @@ -1361,7 +1396,6 @@ class Application(object): self.named_handlers[spec.name] = spec def add_transform(self, transform_class): - """Adds the given OutputTransform to our transform list.""" self.transforms.append(transform_class) def _get_host_handlers(self, request): @@ -1456,11 +1490,11 @@ class Application(object): return handler def reverse_url(self, name, *args): - """Returns a URL path for handler named `name` + """Returns a URL path for handler named ``name`` - The handler must be added to the application as a named URLSpec. + The handler must be added to the application as a named `URLSpec`. - Args will be substituted for capturing groups in the URLSpec regex. + Args will be substituted for capturing groups in the `URLSpec` regex. They will be converted to strings if necessary, encoded as utf8, and url-escaped. """ @@ -1474,7 +1508,7 @@ class Application(object): By default writes to the python root logger. To change this behavior either subclass Application and override this method, or pass a function in the application settings dictionary as - 'log_function'. + ``log_function``. """ if "log_function" in self.settings: self.settings["log_function"](handler) @@ -1493,8 +1527,13 @@ class Application(object): class HTTPError(Exception): """An exception that will turn into an HTTP error response. + Raising an `HTTPError` is a convenient alternative to calling + `RequestHandler.send_error` since it automatically ends the + current function. + :arg int status_code: HTTP status code. Must be listed in - `httplib.responses` unless the ``reason`` keyword argument is given. + `httplib.responses ` unless the ``reason`` + keyword argument is given. :arg string log_message: Message to be written to the log for this error (will not be shown to the user unless the `Application` is in debug mode). May contain ``%s``-style placeholders, which will be filled @@ -1521,7 +1560,7 @@ class HTTPError(Exception): class ErrorHandler(RequestHandler): - """Generates an error response with status_code for all requests.""" + """Generates an error response with ``status_code`` for all requests.""" def initialize(self, status_code): self.set_status(status_code) @@ -1538,7 +1577,7 @@ class ErrorHandler(RequestHandler): class RedirectHandler(RequestHandler): """Redirects the client to the given URL for all GET requests. - You should provide the keyword argument "url" to the handler, e.g.:: + You should provide the keyword argument ``url`` to the handler, e.g.:: application = web.Application([ (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}), @@ -1555,20 +1594,20 @@ class RedirectHandler(RequestHandler): class StaticFileHandler(RequestHandler): """A simple handler that can serve static content from a directory. - To map a path to this handler for a static data directory /var/www, + To map a path to this handler for a static data directory ``/var/www``, you would add a line to your application like:: application = web.Application([ (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}), ]) - The local root directory of the content should be passed as the "path" + The local root directory of the content should be passed as the ``path`` argument to the handler. - To support aggressive browser caching, if the argument "v" is given + To support aggressive browser caching, if the argument ``v`` is given with the path, we set an infinite HTTP expiration header. So, if you want browsers to cache a file indefinitely, send them to, e.g., - /static/images/myimage.png?v=xxx. Override ``get_cache_time`` method for + ``/static/images/myimage.png?v=xxx``. Override `get_cache_time` method for more fine-grained cache control. """ CACHE_MAX_AGE = 86400 * 365 * 10 # 10 years @@ -1609,7 +1648,7 @@ class StaticFileHandler(RequestHandler): raise HTTPError(403, "%s is not a file", path) stat_result = os.stat(abspath) - modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME]) + modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME]) self.set_header("Last-Modified", modified) @@ -1631,7 +1670,7 @@ class StaticFileHandler(RequestHandler): ims_value = self.request.headers.get("If-Modified-Since") if ims_value is not None: date_tuple = email.utils.parsedate(ims_value) - if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple)) + if_since = datetime.datetime(*date_tuple[:6]) if if_since >= modified: self.set_status(304) return @@ -1651,11 +1690,13 @@ class StaticFileHandler(RequestHandler): def get_cache_time(self, path, modified, mime_type): """Override to customize cache control behavior. - Return a positive number of seconds to trigger aggressive caching or 0 - to mark resource as cacheable, only. + Return a positive number of seconds to make the result + cacheable for that amount of time or 0 to mark resource as + cacheable for an unspecified amount of time (subject to + browser heuristics). By default returns cache expiry of 10 years for resources requested - with "v" argument. + with ``v`` argument. """ return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0 @@ -1718,12 +1759,13 @@ class StaticFileHandler(RequestHandler): class FallbackHandler(RequestHandler): - """A RequestHandler that wraps another HTTP server callback. + """A `RequestHandler` that wraps another HTTP server callback. - The fallback is a callable object that accepts an HTTPRequest, - such as an Application or tornado.wsgi.WSGIContainer. This is most - useful to use both tornado RequestHandlers and WSGI in the same server. - Typical usage:: + The fallback is a callable object that accepts an + `~.httpserver.HTTPRequest`, such as an `Application` or + `tornado.wsgi.WSGIContainer`. This is most useful to use both + Tornado ``RequestHandlers`` and WSGI in the same server. Typical + usage:: wsgi_app = tornado.wsgi.WSGIContainer( django.core.handlers.wsgi.WSGIHandler()) @@ -1837,7 +1879,11 @@ class ChunkedTransferEncoding(OutputTransform): def authenticated(method): - """Decorate methods with this to require that the user be logged in.""" + """Decorate methods with this to require that the user be logged in. + + If the user is not logged in, they will be redirected to the configured + `login url `. + """ @functools.wraps(method) def wrapper(self, *args, **kwargs): if not self.current_user: @@ -1858,7 +1904,7 @@ def authenticated(method): class UIModule(object): - """A UI re-usable, modular unit on a page. + """A re-usable, modular UI unit on a page. UI modules often execute additional queries, and they can include additional CSS and JavaScript that will be included in the output @@ -1985,21 +2031,19 @@ class TemplateModule(UIModule): class URLSpec(object): """Specifies mappings between URLs and handlers.""" def __init__(self, pattern, handler_class, kwargs=None, name=None): - """Creates a URLSpec. + """Parameters: - Parameters: + * ``pattern``: Regular expression to be matched. Any groups + in the regex will be passed in to the handler's get/post/etc + methods as arguments. - pattern: Regular expression to be matched. Any groups in the regex - will be passed in to the handler's get/post/etc methods as - arguments. + * ``handler_class``: `RequestHandler` subclass to be invoked. - handler_class: RequestHandler subclass to be invoked. + * ``kwargs`` (optional): A dictionary of additional arguments + to be passed to the handler's constructor. - kwargs (optional): A dictionary of additional arguments to be passed - to the handler's constructor. - - name (optional): A name for this handler. Used by - Application.reverse_url. + * ``name`` (optional): A name for this handler. Used by + `Application.reverse_url`. """ if not pattern.endswith('$'): pattern += '$' diff --git a/libs/tornado/websocket.py b/libs/tornado/websocket.py index 235a1102..7bc65138 100755 --- a/libs/tornado/websocket.py +++ b/libs/tornado/websocket.py @@ -1,4 +1,4 @@ -"""Server-side implementation of the WebSocket protocol. +"""Implementation of the WebSocket protocol. `WebSockets `_ allow for bidirectional communication between the browser and server. @@ -25,46 +25,44 @@ import base64 import collections import functools import hashlib -import logging import os -import re -import socket import struct import time import tornado.escape import tornado.web -from tornado.concurrent import Future, return_future -from tornado.escape import utf8, to_unicode, native_str -from tornado.httputil import HTTPHeaders +from tornado.concurrent import Future +from tornado.escape import utf8, native_str +from tornado import httpclient from tornado.ioloop import IOLoop -from tornado.iostream import IOStream, SSLIOStream from tornado.log import gen_log, app_log from tornado.netutil import Resolver from tornado import simple_httpclient -from tornado.util import bytes_type +from tornado.util import bytes_type, unicode_type try: xrange # py2 except NameError: xrange = range # py3 -try: - import urlparse # py2 -except ImportError: - import urllib.parse as urlparse # py3 + +class WebSocketError(Exception): + pass + class WebSocketHandler(tornado.web.RequestHandler): """Subclass this class to create a basic WebSocket handler. - Override on_message to handle incoming messages. You can also override - open and on_close to handle opened and closed connections. + Override `on_message` to handle incoming messages, and use + `write_message` to send messages to the client. You can also + override `open` and `on_close` to handle opened and closed + connections. See http://dev.w3.org/html5/websockets/ for details on the JavaScript interface. The protocol is specified at http://tools.ietf.org/html/rfc6455. - Here is an example Web Socket handler that echos back all received messages + Here is an example WebSocket handler that echos back all received messages back to the client:: class EchoWebSocket(websocket.WebSocketHandler): @@ -77,14 +75,15 @@ class WebSocketHandler(tornado.web.RequestHandler): def on_close(self): print "WebSocket closed" - Web Sockets are not standard HTTP connections. The "handshake" is HTTP, - but after the handshake, the protocol is message-based. Consequently, - most of the Tornado HTTP facilities are not available in handlers of this - type. The only communication methods available to you are write_message() - and close(). Likewise, your request handler class should - implement open() method rather than get() or post(). + WebSockets are not standard HTTP connections. The "handshake" is + HTTP, but after the handshake, the protocol is + message-based. Consequently, most of the Tornado HTTP facilities + are not available in handlers of this type. The only communication + methods available to you are `write_message()`, `ping()`, and + `close()`. Likewise, your request handler class should implement + `open()` method rather than ``get()`` or ``post()``. - If you map the handler above to "/websocket" in your application, you can + If you map the handler above to ``/websocket`` in your application, you can invoke it in JavaScript with:: var ws = new WebSocket("ws://localhost:8888/websocket"); @@ -211,6 +210,7 @@ class WebSocketHandler(tornado.web.RequestHandler): Once the close handshake is successful the socket will be closed. """ self.ws_connection.close() + self.ws_connection = None def allow_draft76(self): """Override to enable support for the older "draft76" protocol. @@ -240,11 +240,10 @@ class WebSocketHandler(tornado.web.RequestHandler): return "wss" if self.request.protocol == "https" else "ws" def async_callback(self, callback, *args, **kwargs): - """Wrap callbacks with this if they are used on asynchronous requests. + """Obsolete - catches exceptions from the wrapped function. - Catches exceptions properly and closes this WebSocket if an exception - is uncaught. (Note that this is usually unnecessary thanks to - `tornado.stack_context`) + This function is normally unncecessary thanks to + `tornado.stack_context`. """ return self.ws_connection.async_callback(callback, *args, **kwargs) @@ -398,8 +397,9 @@ class WebSocketProtocol76(WebSocketProtocol): """Processes the key headers and calculates their key value. Raises ValueError when feed invalid key.""" + # pyflakes complains about variable reuse if both of these lines use 'c' number = int(''.join(c for c in key if c.isdigit())) - spaces = len([c for c in key if c.isspace()]) + spaces = len([c2 for c2 in key if c2.isspace()]) try: key_number = number // spaces except (ValueError, ZeroDivisionError): @@ -444,7 +444,7 @@ class WebSocketProtocol76(WebSocketProtocol): if binary: raise ValueError( "Binary messages not supported by this version of websockets") - if isinstance(message, unicode): + if isinstance(message, unicode_type): message = message.encode("utf-8") assert isinstance(message, bytes_type) self.stream.write(b"\x00" + message + b"\xff") @@ -520,7 +520,6 @@ class WebSocketProtocol13(WebSocketProtocol): return WebSocketProtocol13.compute_accept_value( self.request.headers.get("Sec-Websocket-Key")) - def _accept_connection(self): subprotocol_header = '' subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '') @@ -727,7 +726,8 @@ class WebSocketProtocol13(WebSocketProtocol): self.stream.io_loop.time() + 5, self._abort) -class _WebSocketClientConnection(simple_httpclient._HTTPConnection): +class WebSocketClientConnection(simple_httpclient._HTTPConnection): + """WebSocket client connection.""" def __init__(self, io_loop, request): self.connect_future = Future() self.read_future = None @@ -738,19 +738,26 @@ class _WebSocketClientConnection(simple_httpclient._HTTPConnection): scheme = {'ws': 'http', 'wss': 'https'}[scheme] request.url = scheme + sep + rest request.headers.update({ - 'Upgrade': 'websocket', - 'Connection': 'Upgrade', - 'Sec-WebSocket-Key': self.key, - 'Sec-WebSocket-Version': '13', - }) + 'Upgrade': 'websocket', + 'Connection': 'Upgrade', + 'Sec-WebSocket-Key': self.key, + 'Sec-WebSocket-Version': '13', + }) - super(_WebSocketClientConnection, self).__init__( - io_loop, None, request, lambda: None, lambda response: None, + super(WebSocketClientConnection, self).__init__( + io_loop, None, request, lambda: None, self._on_http_response, 104857600, Resolver(io_loop=io_loop)) def _on_close(self): self.on_message(None) + def _on_http_response(self, response): + if not self.connect_future.done(): + if response.error: + self.connect_future.set_exception(response.error) + else: + self.connect_future.set_exception(WebSocketError( + "Non-websocket response")) def _handle_1xx(self, code): assert code == 101 @@ -769,9 +776,17 @@ class _WebSocketClientConnection(simple_httpclient._HTTPConnection): self.connect_future.set_result(self) def write_message(self, message, binary=False): + """Sends a message to the WebSocket server.""" self.protocol.write_message(message, binary) def read_message(self, callback=None): + """Reads a message from the WebSocket server. + + Returns a future whose result is the message, or None + if the connection is closed. If a callback argument + is given it will be called with the future when it is + ready. + """ assert self.read_future is None future = Future() if self.read_queue: @@ -793,13 +808,18 @@ class _WebSocketClientConnection(simple_httpclient._HTTPConnection): pass -def WebSocketConnect(url, io_loop=None, callback=None): +def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None): + """Client-side websocket support. + + Takes a url and returns a Future whose result is a + `WebSocketClientConnection`. + """ if io_loop is None: - io_loop = IOLoop.instance() - request = simple_httpclient.HTTPRequest(url) - request = simple_httpclient._RequestProxy( - request, simple_httpclient.HTTPRequest._DEFAULTS) - conn = _WebSocketClientConnection(io_loop, request) + io_loop = IOLoop.current() + request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout) + request = httpclient._RequestProxy( + request, httpclient.HTTPRequest._DEFAULTS) + conn = WebSocketClientConnection(io_loop, request) if callback is not None: io_loop.add_future(conn.connect_future, callback) return conn.connect_future diff --git a/libs/tornado/wsgi.py b/libs/tornado/wsgi.py index 3d06860f..62cff590 100755 --- a/libs/tornado/wsgi.py +++ b/libs/tornado/wsgi.py @@ -82,11 +82,11 @@ else: class WSGIApplication(web.Application): """A WSGI equivalent of `tornado.web.Application`. - WSGIApplication is very similar to web.Application, except no - asynchronous methods are supported (since WSGI does not support - non-blocking requests properly). If you call self.flush() or other - asynchronous methods in your request handlers running in a - WSGIApplication, we throw an exception. + `WSGIApplication` is very similar to `tornado.web.Application`, + except no asynchronous methods are supported (since WSGI does not + support non-blocking requests properly). If you call + ``self.flush()`` or other asynchronous methods in your request + handlers running in a `WSGIApplication`, we throw an exception. Example usage:: @@ -105,13 +105,15 @@ class WSGIApplication(web.Application): server = wsgiref.simple_server.make_server('', 8888, application) server.serve_forever() - See the 'appengine' demo for an example of using this module to run - a Tornado app on Google AppEngine. + See the `appengine demo + `_ + for an example of using this module to run a Tornado app on Google + App Engine. - Since no asynchronous methods are available for WSGI applications, the - httpclient and auth modules are both not available for WSGI applications. - We support the same interface, but handlers running in a WSGIApplication - do not support flush() or asynchronous methods. + WSGI applications use the same `.RequestHandler` class, but not + ``@asynchronous`` methods or ``flush()``. This means that it is + not possible to use `.AsyncHTTPClient`, or the `tornado.auth` or + `tornado.websocket` modules. """ def __init__(self, handlers=None, default_host="", **settings): web.Application.__init__(self, handlers, default_host, transforms=[], @@ -134,7 +136,7 @@ class WSGIApplication(web.Application): class HTTPRequest(object): """Mimics `tornado.httpserver.HTTPRequest` for WSGI applications.""" def __init__(self, environ): - """Parses the given WSGI environ to construct the request.""" + """Parses the given WSGI environment to construct the request.""" self.method = environ["REQUEST_METHOD"] self.path = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", ""))) self.path += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) @@ -206,7 +208,7 @@ class HTTPRequest(object): class WSGIContainer(object): r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server. - Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to + Wrap a WSGI function in a `WSGIContainer` and pass it to `.HTTPServer` to run it. For example:: def simple_app(environ, start_response): From 2979a8edecb786330a135b13f58c4cbb8e4109b1 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Fri, 19 Apr 2013 21:22:29 +0200 Subject: [PATCH 071/127] Cleanup --- couchpotato/core/plugins/renamer/main.py | 65 ++++++++++++++---------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 807ee04c..a8b39c7f 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -103,7 +103,7 @@ class Renamer(Plugin): # make sure the movie folder name is included in the search folder = None - movie_files = [] + files = [] if movie_folder: log.info('Scanning movie folder %s...', movie_folder) movie_folder = movie_folder.rstrip(os.path.sep) @@ -112,38 +112,17 @@ class Renamer(Plugin): # Get all files from the specified folder try: for root, folders, names in os.walk(movie_folder): - movie_files.extend([os.path.join(root, name) for name in names]) + files.extend([os.path.join(root, name) for name in names]) except: log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc())) db = get_session() - # Get the release with the downloader ID that was downloaded by the downloader - download_info = None - if download_id and downloader: - rls = None - - rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader).all() - rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).all() - - for rlsnfo_dwnld in rlsnfo_dwnlds: - for rlsnfo_id in rlsnfo_ids: - if rlsnfo_id.release == rlsnfo_dwnld.release: - rls = rlsnfo_id.release - break - if rls: break - - if rls: - download_info = { - 'imdb_id': rls.movie.library.identifier, - 'quality': rls.quality.identifier, - 'is_torrent': any(downloader_type in fireEvent('download.downloader_type', downloader = downloader) for downloader_type in ['torrent', 'torrent_magnet']) - } - else: - log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) + # Get the download info stored in the downloaded release + download_info = getDownloadInfo(download_id = download_id, downloader = downloader) groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), - files = movie_files, download_info = download_info, return_ignored = False, single = True) + files = files, download_info = download_info, return_ignored = False, single = True) destination = self.conf('to') folder_name = self.conf('folder_name') @@ -678,7 +657,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) if item['id'] and item['downloader'] and item['folder']: - fireEventAsync('renamer.scan', movie_folder = item['folder'], downloader = item['downloader'], download_id = item['id']) + fireEvent('renamer.scan', movie_folder = item['folder'], downloader = item['downloader'], download_id = item['id']) else: scan_required = True @@ -697,3 +676,35 @@ Remove it if you want it to be renamed (again, or at least let it try again) self.checking_snatched = False return True + +def getDownloadInfo(self, download_id, downloader): + + rls = None + download_info = None + + if download_id and downloader: + + db = get_session() + + rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader).all() + rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).all() + + for rlsnfo_dwnld in rlsnfo_dwnlds: + for rlsnfo_id in rlsnfo_ids: + if rlsnfo_id.release == rlsnfo_dwnld.release: + rls = rlsnfo_id.release + break + if rls: break + + if not rls: + log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) + + if rls: + download_info = { + 'imdb_id': rls.movie.library.identifier, + 'quality': rls.quality.identifier, + 'is_torrent': any(downloader_type in fireEvent('download.downloader_type', downloader = downloader) for downloader_type in ['torrent', 'torrent_magnet']) + } + + return download_info + From f2f43a223142785db739cf80fd75d03a31471fd5 Mon Sep 17 00:00:00 2001 From: Clinton Hall Date: Tue, 16 Apr 2013 11:10:50 +0930 Subject: [PATCH 072/127] Wont delete the "from" (sub)folders, "movie" folder or "destination" folder at this time. These should be deleted after the renaming... at this stage we should only be deleting any "older releases" --- couchpotato/core/plugins/renamer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 87d9aaf3..8ad7ddc2 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -421,7 +421,7 @@ class Renamer(Plugin): os.remove(src) parent_dir = os.path.normpath(os.path.dirname(src)) - if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir: + if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not self.conf('from') in parent_dir: delete_folders.append(parent_dir) except: From b85942989dcd69e5ed8a3c4a9b38edd9081c89de Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sun, 21 Apr 2013 23:46:21 +0200 Subject: [PATCH 073/127] Standardise failed status apply the failed status in case of a manual re-add and automatic try next release --- couchpotato/core/plugins/renamer/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index a8b39c7f..47080cbc 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -647,13 +647,12 @@ Remove it if you want it to be renamed (again, or at least let it try again) pass elif item['status'] == 'failed': fireEvent('download.remove_failed', item, single = True) + rel.status_id = failed_status.get('id') + rel.last_edit = int(time.time()) + db.commit() if self.conf('next_on_failed'): fireEvent('searcher.try_next_release', movie_id = rel.movie_id) - else: - rel.status_id = failed_status.get('id') - rel.last_edit = int(time.time()) - db.commit() elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) if item['id'] and item['downloader'] and item['folder']: From 1022753213976a6650505dfa37ddbdeaf5fd990d Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Wed, 24 Apr 2013 22:19:34 +0200 Subject: [PATCH 074/127] Add username to nzbget downloader For raspberry pi a different username than normal is required. Fixes #1652 --- couchpotato/core/downloaders/nzbget/__init__.py | 4 ++++ couchpotato/core/downloaders/nzbget/main.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 5349914a..bbe52ca4 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -24,6 +24,10 @@ config = [{ 'default': 'localhost:6789', 'description': 'Hostname with port. Usually localhost:6789', }, + { + 'name': 'username', + 'default': 'nzbget', + }, { 'name': 'password', 'type': 'password', diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 78e2b957..ff307432 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -16,7 +16,7 @@ class NZBGet(Downloader): type = ['nzb'] - url = 'http://nzbget:%(password)s@%(host)s/xmlrpc' + url = 'http://%(username):%(password)s@%(host)s/xmlrpc' def download(self, data = {}, movie = {}, filedata = None): @@ -26,7 +26,7 @@ class NZBGet(Downloader): log.info('Sending "%s" to NZBGet.', data.get('name')) - url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} + url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} nzb_name = ss('%s.nzb' % self.createNzbName(data, movie)) rpc = xmlrpclib.ServerProxy(url) @@ -61,7 +61,7 @@ class NZBGet(Downloader): log.debug('Checking NZBGet download status.') - url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} + url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} rpc = xmlrpclib.ServerProxy(url) try: From 92998bafc836ff021f647a5d65063716c47eefbe Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Sat, 20 Apr 2013 11:49:10 +0930 Subject: [PATCH 075/127] write unique id to nzbget params My mistake. Fixed now. Yeah... sorry ;) This does work for check_snatched... Marks as busy, or failed etc. keep consistent release table format fix check_snatched correctly parse the NZBGet Parameters and Pass status.downloader remove downloader and fix id My mistake. Fixed now. Yeah... sorry ;) This does work for check_snatched... Marks as busy, or failed etc. keep consistent release table format fix check_snatched correctly parse the NZBGet Parameters and Pass status.downloader remove downloader and fix id --- couchpotato/core/downloaders/nzbget/main.py | 32 ++++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 78e2b957..5478a3cb 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -1,7 +1,7 @@ from base64 import standard_b64encode from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import ss -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, md5 from couchpotato.core.logger import CPLog from datetime import timedelta import re @@ -52,7 +52,14 @@ class NZBGet(Downloader): if xml_response: log.info('NZB sent successfully to NZBGet') - return True + nzb_id = md5(data['url']) # about as unique as they come ;) + couchpotato_id = "couchpotato=" + nzb_id + groups = rpc.listgroups() + file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name] + confirmed = rpc.editqueue("GroupSetParameter", 0, couchpotato_id, file_id) + if confirmed: + log.debug('couchpotato parameter set in nzbget download') + return self.downloadReturnId(nzb_id) else: log.error('NZBGet could not add %s to the queue.', nzb_name) return False @@ -93,15 +100,19 @@ class NZBGet(Downloader): for item in groups: log.debug('Found %s in NZBGet download queue', item['NZBFilename']) + try: + NZB_ID = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] + except: + NZB_ID = item['NZBID'], statuses.append({ - 'id': item['NZBID'], + 'id': NZB_ID, 'name': item['NZBFilename'], 'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED', # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item 'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1, }) - for item in queue: + for item in queue: # 'Parameters' is not passed in rpc.postqueue log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename']) statuses.append({ 'id': item['NZBID'], @@ -112,8 +123,12 @@ class NZBGet(Downloader): for item in history: log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log'])) + try: + NZB_ID = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] + except: + NZB_ID = item['NZBID'], statuses.append({ - 'id': item['NZBID'], + 'id': NZB_ID, 'name': item['NZBFilename'], 'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed', 'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'], @@ -147,8 +162,11 @@ class NZBGet(Downloader): try: history = rpc.history() - if rpc.editqueue('HistoryDelete', 0, "", [tryInt(item['id'])]): - path = [hist['DestDir'] for hist in history if hist['NZBID'] == item['id']][0] + for hist in history: + if hist['Parameters'] and hist['Parameters']['couchpotato'] and hist['Parameters']['couchpotato'] == item['id']: + NZBID = hist['ID'] + path = hist['DestDir'] + if rpc.editqueue('HistoryDelete', 0, "", [tryInt(NZBID)]): shutil.rmtree(path, True) except: log.error('Failed deleting: %s', traceback.format_exc(0)) From cbd29df52afcf07f84889674d8621cb8d0e7a813 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 26 Apr 2013 16:27:36 +0200 Subject: [PATCH 076/127] Update to_go even if movie isn't found in manage. --- couchpotato/core/plugins/manage/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 51094899..3e791de1 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -185,6 +185,8 @@ class Manage(Plugin): # Add it to release and update the info fireEvent('release.add', group = group) fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier)) + else: + self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1 return addToLibrary From 1600b6d0ea21b3eb146682dba1910b811aaab1ff Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 26 Apr 2013 16:36:42 +0200 Subject: [PATCH 077/127] NZBGet set advanced and description --- couchpotato/core/downloaders/nzbget/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index bbe52ca4..a17b2dc5 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -27,6 +27,8 @@ config = [{ { 'name': 'username', 'default': 'nzbget', + 'advanced': True, + 'description': 'Set a different username to connect. Default: nzbget', }, { 'name': 'password', From 518ac1681445966949eaad83213b17b7990dc810 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 26 Apr 2013 16:44:02 +0200 Subject: [PATCH 078/127] Use lowercase variable --- couchpotato/core/downloaders/nzbget/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 7dfa472f..0f1bfcdf 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -58,7 +58,7 @@ class NZBGet(Downloader): file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name] confirmed = rpc.editqueue("GroupSetParameter", 0, couchpotato_id, file_id) if confirmed: - log.debug('couchpotato parameter set in nzbget download') + log.debug('couchpotato parameter set in nzbget download') return self.downloadReturnId(nzb_id) else: log.error('NZBGet could not add %s to the queue.', nzb_name) @@ -101,11 +101,11 @@ class NZBGet(Downloader): for item in groups: log.debug('Found %s in NZBGet download queue', item['NZBFilename']) try: - NZB_ID = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] + nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] except: - NZB_ID = item['NZBID'], + nzb_id = item['NZBID'], statuses.append({ - 'id': NZB_ID, + 'id': nzb_id, 'name': item['NZBFilename'], 'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED', # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item @@ -124,11 +124,11 @@ class NZBGet(Downloader): for item in history: log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log'])) try: - NZB_ID = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] + nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] except: - NZB_ID = item['NZBID'], + nzb_id = item['NZBID'], statuses.append({ - 'id': NZB_ID, + 'id': nzb_id, 'name': item['NZBFilename'], 'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed', 'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'], From e786c9c79a8f41a205f34dcf8770294c2a661ea3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 26 Apr 2013 18:11:18 +0200 Subject: [PATCH 079/127] Use sets for ignored words. fixes #1657 --- couchpotato/core/plugins/searcher/__init__.py | 3 ++- couchpotato/core/plugins/searcher/main.py | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index 90cece46..aec419a2 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -25,12 +25,13 @@ config = [{ 'label': 'Required words', 'default': '', 'placeholder': 'Example: DTS, AC3 & English', - 'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' + 'description': 'A release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' }, { 'name': 'ignored_words', 'label': 'Ignored words', 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', + 'description': 'Ignores releases that match any of these sets. (Works like explained above)' }, { 'name': 'preferred_method', diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index ad33c6ff..e6e81740 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -384,8 +384,9 @@ class Searcher(Plugin): movie_words = re.split('\W+', simplifyString(movie_name)) nzb_name = simplifyString(nzb['name']) nzb_words = re.split('\W+', nzb_name) - required_words = splitString(self.conf('required_words').lower()) + # Make sure it has required words + required_words = splitString(self.conf('required_words').lower()) req_match = 0 for req_set in required_words: req = splitString(req_set, '&') @@ -395,19 +396,24 @@ class Searcher(Plugin): log.info2("Wrong: Required word missing: %s" % nzb['name']) return False + # Ignore releases ignored_words = splitString(self.conf('ignored_words').lower()) - blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words)) - if self.conf('ignored_words') and blacklisted: - log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted))) + ignored_match = 0 + for ignored_set in ignored_words: + ignored = splitString(ignored_set, '&') + ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored) + + if self.conf('ignored_words') and ignored_match: + log.info2("Wrong: '%s' contains 'ignored words'" % (nzb['name'])) return False + # Ignore porn stuff pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic'] pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words)) if pron_words: log.info('Wrong: %s, probably pr0n', (nzb['name'])) return False - #qualities = fireEvent('quality.all', single = True) preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) # Contains lower quality string From 6e45c14ac5f372bbff8768366d80891016c6d267 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 26 Apr 2013 18:49:39 +0200 Subject: [PATCH 080/127] Don't download html files as trailers. fixes #1658 --- couchpotato/core/plugins/trailer/__init__.py | 2 +- couchpotato/core/plugins/trailer/main.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py index e4c51bd1..d8496b30 100644 --- a/couchpotato/core/plugins/trailer/__init__.py +++ b/couchpotato/core/plugins/trailer/__init__.py @@ -29,7 +29,7 @@ config = [{ 'label': 'Naming', 'default': '-trailer', 'advanced': True, - 'description': 'Use to use above settings.' + 'description': 'Use <filename> to use above settings.' }, ], }, diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer/main.py index 4ab51e78..1a8955fb 100644 --- a/couchpotato/core/plugins/trailer/main.py +++ b/couchpotato/core/plugins/trailer/main.py @@ -22,10 +22,15 @@ class Trailer(Plugin): return False for trailer in trailers.get(self.conf('quality'), []): - filename = self.conf('name').replace('', group['filename']) + ('.%s' % getExt(trailer)) + + ext = getExt(trailer) + filename = self.conf('name').replace('', group['filename']) + ('.%s' % ('mp4' if len(ext) > 5 else ext)) destination = os.path.join(group['destination_dir'], filename) if not os.path.isfile(destination): - fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True) + trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True) + if os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one + os.unlink(trailer_file) + continue else: log.debug('Trailer already exists: %s', destination) From 6ee68d1418204f5d504578226ee9ea42272cedc6 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Fri, 26 Apr 2013 19:34:15 +0200 Subject: [PATCH 081/127] Fix getDownloadInfo --- couchpotato/core/plugins/renamer/main.py | 67 ++++++++++++------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 47080cbc..a84f36e3 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -119,7 +119,7 @@ class Renamer(Plugin): db = get_session() # Get the download info stored in the downloaded release - download_info = getDownloadInfo(download_id = download_id, downloader = downloader) + download_info = self.getDownloadInfo(download_id = download_id, downloader = downloader) groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = files, download_info = download_info, return_ignored = False, single = True) @@ -355,7 +355,7 @@ class Renamer(Plugin): else: log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label)) - # Add _EXISTS_ to the parent dir + # Add exists tag to the .ignore file self.tagDir(group, 'exists') # Notify on rename fail @@ -376,7 +376,8 @@ class Renamer(Plugin): db.commit() # Remove leftover files - if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers: + if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \ + not (self.conf('file_action') != 'move' and download_info and download_info.get('is_torrent')): log.debug('Removing leftover files') for current_file in group['files']['leftover']: remove_files.append(current_file) @@ -676,34 +677,34 @@ Remove it if you want it to be renamed (again, or at least let it try again) return True -def getDownloadInfo(self, download_id, downloader): - - rls = None - download_info = None - - if download_id and downloader: - - db = get_session() - - rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader).all() - rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).all() - - for rlsnfo_dwnld in rlsnfo_dwnlds: - for rlsnfo_id in rlsnfo_ids: - if rlsnfo_id.release == rlsnfo_dwnld.release: - rls = rlsnfo_id.release - break - if rls: break - - if not rls: - log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) - - if rls: - download_info = { - 'imdb_id': rls.movie.library.identifier, - 'quality': rls.quality.identifier, - 'is_torrent': any(downloader_type in fireEvent('download.downloader_type', downloader = downloader) for downloader_type in ['torrent', 'torrent_magnet']) - } - - return download_info + def getDownloadInfo(self, download_id, downloader): + + rls = None + download_info = None + + if download_id and downloader: + + db = get_session() + + rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader).all() + rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).all() + + for rlsnfo_dwnld in rlsnfo_dwnlds: + for rlsnfo_id in rlsnfo_ids: + if rlsnfo_id.release == rlsnfo_dwnld.release: + rls = rlsnfo_id.release + break + if rls: break + + if not rls: + log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) + + if rls: + download_info = { + 'imdb_id': rls.movie.library.identifier, + 'quality': rls.quality.identifier, + 'is_torrent': any(downloader_type in fireEvent('download.downloader_type', downloader = downloader) for downloader_type in ['torrent', 'torrent_magnet']) + } + + return download_info From 384a355a53d32b49a8d6ad9d506deb347974a48c Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 26 Apr 2013 23:48:54 +0200 Subject: [PATCH 082/127] Check release type by info --- couchpotato/core/downloaders/base.py | 7 --- couchpotato/core/plugins/renamer/main.py | 56 ++++++++++++------------ 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index a78e0b18..8ccc1369 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -39,13 +39,6 @@ class Downloader(Provider): addEvent('download.enabled_types', self.getEnabledDownloadType) addEvent('download.status', self._getAllDownloadStatus) addEvent('download.remove_failed', self._removeFailed) - addEvent('download.downloader_type', self.getDownloaderType) - - def getDownloaderType(self, downloader): - if self.getName() == downloader: - return self.type - - return [] def getEnabledDownloadType(self): for download_type in self.type: diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 3842b855..1a8be7af 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -70,15 +70,14 @@ class Renamer(Plugin): fireEventAsync('renamer.scan', movie_folder = movie_folder, - downloader = downloader, - download_id = download_id + download_info = {'id': download_id, 'downloader': downloader} if download_id else None ) return jsonified({ 'success': True }) - def scan(self, movie_folder = None, downloader = None, download_id = None): + def scan(self, movie_folder = None, download_info = None): if self.isDisabled(): return @@ -118,8 +117,8 @@ class Renamer(Plugin): db = get_session() - # Get the download info stored in the downloaded release - download_info = self.getDownloadInfo(download_id = download_id, downloader = downloader) + # Extend the download info with info stored in the downloaded release + download_info = self.extendDownloadInfo(download_info) groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = files, download_info = download_info, return_ignored = False, single = True) @@ -377,7 +376,7 @@ class Renamer(Plugin): # Remove leftover files if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \ - not (self.conf('file_action') != 'move' and download_info and download_info.get('is_torrent')): + not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)): log.debug('Removing leftover files') for current_file in group['files']['leftover']: remove_files.append(current_file) @@ -427,13 +426,13 @@ class Renamer(Plugin): self.makeDir(os.path.dirname(dst)) try: - self.moveFile(src, dst, forcemove = not (download_info and download_info.get('is_torrent'))) + self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info)) group['renamed_files'].append(dst) except: log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) self.tagDir(group, 'failed_rename') - if self.conf('file_action') != 'move' and download_info and download_info.get('is_torrent'): + if self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info): self.tagDir(group, 'renamed already') # Remove matching releases @@ -657,7 +656,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) if item['id'] and item['downloader'] and item['folder']: - fireEvent('renamer.scan', movie_folder = item['folder'], downloader = item['downloader'], download_id = item['id']) + fireEventAsync('renamer.scan', movie_folder = item['folder'], download_info = item) else: scan_required = True @@ -677,34 +676,37 @@ Remove it if you want it to be renamed (again, or at least let it try again) return True - def getDownloadInfo(self, download_id, downloader): - - rls = None - download_info = None - - if download_id and downloader: - + def extendDownloadInfo(self, download_info): + + rls = None + + if download_info and download_info.get('id') and download_info.get('downloader'): + db = get_session() - - rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader).all() - rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).all() - + + rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = download_info.get('downloader')).all() + rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_info.get('id')).all() + for rlsnfo_dwnld in rlsnfo_dwnlds: for rlsnfo_id in rlsnfo_ids: if rlsnfo_id.release == rlsnfo_dwnld.release: rls = rlsnfo_id.release break if rls: break - + if not rls: - log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) - + log.error('Download ID %s from downloader %s not found in releases', (download_info.get('id'), download_info.get('downloader'))) + if rls: - download_info = { + + rls_dict = rls.to_dict({'info':{}}) + download_info.update({ 'imdb_id': rls.movie.library.identifier, 'quality': rls.quality.identifier, - 'is_torrent': any(downloader_type in fireEvent('download.downloader_type', downloader = downloader) for downloader_type in ['torrent', 'torrent_magnet']) - } - + 'type': rls_dict.get('info', {}).get('type') + }) + return download_info + def downloadIsTorrent(self, download_info): + return download_info and download_info.get('type') in ['torrent', 'torrent_magnet'] From 1a846b04ee63d2f4cdeb40abe80d4e85328653ff Mon Sep 17 00:00:00 2001 From: Clinton Hall Date: Sat, 27 Apr 2013 11:26:52 +0930 Subject: [PATCH 083/127] Fix minor errors. Fix #1666 --- couchpotato/core/downloaders/nzbget/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 0f1bfcdf..c060a334 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -16,7 +16,7 @@ class NZBGet(Downloader): type = ['nzb'] - url = 'http://%(username):%(password)s@%(host)s/xmlrpc' + url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc' def download(self, data = {}, movie = {}, filedata = None): @@ -103,7 +103,7 @@ class NZBGet(Downloader): try: nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] except: - nzb_id = item['NZBID'], + nzb_id = item['NZBID'] statuses.append({ 'id': nzb_id, 'name': item['NZBFilename'], @@ -126,7 +126,7 @@ class NZBGet(Downloader): try: nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] except: - nzb_id = item['NZBID'], + nzb_id = item['NZBID'] statuses.append({ 'id': nzb_id, 'name': item['NZBFilename'], From 3936100000c9693916f892e3ab0798c916122016 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 27 Apr 2013 09:42:29 +0200 Subject: [PATCH 084/127] Cache status calls --- couchpotato/core/plugins/status/main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index c01caef5..f9fb5138 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -25,6 +25,7 @@ class StatusPlugin(Plugin): 'available': 'Available', 'suggest': 'Suggest', } + status_cached = {} def __init__(self): addEvent('status.add', self.add) @@ -32,6 +33,7 @@ class StatusPlugin(Plugin): addEvent('status.get_by_id', self.getById) addEvent('status.all', self.all) addEvent('app.initialize', self.fill) + addEvent('app.load', self.all) addApiView('status.list', self.list, docs = { 'desc': 'Check for available update', @@ -67,11 +69,16 @@ class StatusPlugin(Plugin): s = status.to_dict() temp.append(s) - #db.close() + # Update cache + self.status_cached[status.identifier] = s + return temp def add(self, identifier): + if self.status_cached.get(identifier): + return self.status_cached.get(identifier) + db = get_session() s = db.query(Status).filter_by(identifier = identifier).first() @@ -85,7 +92,7 @@ class StatusPlugin(Plugin): status_dict = s.to_dict() - #db.close() + self.status_cached[identifier] = status_dict return status_dict def fill(self): From 7818b430456d05e857c8a361314662253133c3f4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 27 Apr 2013 10:08:03 +0200 Subject: [PATCH 085/127] Release files add in bulk --- couchpotato/core/plugins/release/main.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 68bf60a6..5c4d39f2 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -41,6 +41,7 @@ class Release(Plugin): addEvent('release.clean', self.clean) def add(self, group): + db = get_session() identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) @@ -76,15 +77,19 @@ class Release(Plugin): db.commit() # Add each file type + added_files = [] for type in group['files']: for cur_file in group['files'][type]: added_file = self.saveFile(cur_file, type = type, include_media_info = type is 'movie') - try: - added_file = db.query(File).filter_by(id = added_file.get('id')).one() - rel.files.append(added_file) - db.commit() - except Exception, e: - log.debug('Failed to attach "%s" to release: %s', (cur_file, e)) + added_files.append(added_file.get('id')) + + # Add the release files in batch + try: + added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all() + rel.files.append(added_files) + db.commit() + except Exception, e: + log.debug('Failed to attach "%s" to release: %s', (cur_file, e)) fireEvent('movie.restatus', movie.id) From 1d603e1ec294fa243fd6055136440817bcae1cf2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 27 Apr 2013 10:48:47 +0200 Subject: [PATCH 086/127] Simplify event handling --- couchpotato/core/event.py | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index 19e97541..8a5145a6 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -16,10 +16,8 @@ def runHandler(name, handler, *args, **kwargs): def addEvent(name, handler, priority = 100): - if events.get(name): - e = events[name] - else: - e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock()) + if not events.get(name): + events[name] = [] def createHandle(*args, **kwargs): @@ -35,7 +33,10 @@ def addEvent(name, handler, priority = 100): return h - e.handle(createHandle, priority = priority) + events[name].append({ + 'handler': createHandle, + 'priority': priority, + }) def removeEvent(name, handler): e = events[name] @@ -43,6 +44,12 @@ def removeEvent(name, handler): def fireEvent(name, *args, **kwargs): if not events.get(name): return + + e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock()) + + for event in events[name]: + e.handle(event['handler'], priority = event['priority']) + #log.debug('Firing event %s', name) try: @@ -52,6 +59,7 @@ def fireEvent(name, *args, **kwargs): 'single': False, # Return single handler 'merge': False, # Merge items 'in_order': False, # Fire them in specific order, waits for the other to finish + 'async': False } # Do options @@ -62,13 +70,6 @@ def fireEvent(name, *args, **kwargs): options[x] = val except: pass - e = events[name] - - # Lock this event - e.lock.acquire() - - e.asynchronous = False - # Make sure only 1 event is fired at a time when order is wanted kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None kwargs['event_return_on_result'] = options['single'] @@ -76,9 +77,6 @@ def fireEvent(name, *args, **kwargs): # Fire result = e(*args, **kwargs) - # Release lock for this event - e.lock.release() - if options['single'] and not options['merge']: results = None @@ -140,13 +138,8 @@ def fireEvent(name, *args, **kwargs): log.error('%s: %s', (name, traceback.format_exc())) def fireEventAsync(*args, **kwargs): - try: - my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs) - my_thread.setDaemon(True) - my_thread.start() - return True - except Exception, e: - log.error('%s: %s', (args[0], e)) + kwargs['async'] = True + fireEvent(*args, **kwargs) def errorHandler(error): etype, value, tb = error From 9ba19d27a680309102941c51a3c39558cc25bb3c Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 27 Apr 2013 11:04:19 +0200 Subject: [PATCH 087/127] Combine status calls --- couchpotato/core/plugins/movie/main.py | 15 +++---- couchpotato/core/plugins/release/main.py | 11 +++-- couchpotato/core/plugins/renamer/main.py | 13 ++---- couchpotato/core/plugins/searcher/main.py | 3 +- couchpotato/core/plugins/status/main.py | 42 +++++++++++-------- .../core/providers/movie/_modifier/main.py | 3 +- 6 files changed, 41 insertions(+), 46 deletions(-) diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 0a99fe6a..4b595dc5 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -108,9 +108,8 @@ class MoviePlugin(Plugin): now = time.time() week = 262080 - done_status = fireEvent('status.get', 'done', single = True) - available_status = fireEvent('status.get', 'available', single = True) - snatched_status = fireEvent('status.get', 'snatched', single = True) + done_status, available_status, snatched_status = \ + fireEvent('status.get', ['done', 'available', 'snatched'], single = True) db = get_session() @@ -367,11 +366,8 @@ class MoviePlugin(Plugin): library = fireEvent('library.add', single = True, attrs = params, update_after = update_library) # Status - status_active = fireEvent('status.add', 'active', single = True) - snatched_status = fireEvent('status.add', 'snatched', single = True) - ignored_status = fireEvent('status.add', 'ignored', single = True) - done_status = fireEvent('status.add', 'done', single = True) - downloaded_status = fireEvent('status.add', 'downloaded', single = True) + status_active, snatched_status, ignored_status, done_status, downloaded_status = \ + fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True) default_profile = fireEvent('profile.default', single = True) @@ -549,8 +545,7 @@ class MoviePlugin(Plugin): def restatus(self, movie_id): - active_status = fireEvent('status.get', 'active', single = True) - done_status = fireEvent('status.get', 'done', single = True) + active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) db = get_session() diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 5c4d39f2..4967bce0 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -46,8 +46,10 @@ class Release(Plugin): identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) + + done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True) + # Add movie - done_status = fireEvent('status.get', 'done', single = True) movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first() if not movie: movie = Movie( @@ -59,7 +61,6 @@ class Release(Plugin): db.commit() # Add Release - snatched_status = fireEvent('status.get', 'snatched', single = True) rel = db.query(Relea).filter( or_( Relea.identifier == identifier, @@ -152,8 +153,7 @@ class Release(Plugin): rel = db.query(Relea).filter_by(id = id).first() if rel: - ignored_status = fireEvent('status.get', 'ignored', single = True) - available_status = fireEvent('status.get', 'available', single = True) + ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True) rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') db.commit() @@ -166,8 +166,7 @@ class Release(Plugin): db = get_session() id = getParam('id') - snatched_status = fireEvent('status.add', 'snatched', single = True) - done_status = fireEvent('status.get', 'done', single = True) + snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True) rel = db.query(Relea).filter_by(id = id).first() if rel: diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 1a8be7af..5e886fb2 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -131,10 +131,8 @@ class Renamer(Plugin): separator = self.conf('separator') # Statusses - done_status = fireEvent('status.get', 'done', single = True) - active_status = fireEvent('status.get', 'active', single = True) - downloaded_status = fireEvent('status.get', 'downloaded', single = True) - snatched_status = fireEvent('status.get', 'snatched', single = True) + done_status, active_status, downloaded_status, snatched_status = \ + fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched'], single = True) for group_identifier in groups: @@ -588,11 +586,8 @@ Remove it if you want it to be renamed (again, or at least let it try again) self.checking_snatched = True - snatched_status = fireEvent('status.get', 'snatched', single = True) - ignored_status = fireEvent('status.get', 'ignored', single = True) - failed_status = fireEvent('status.get', 'failed', single = True) - - done_status = fireEvent('status.get', 'done', single = True) + snatched_status, ignored_status, failed_status, done_status = \ + fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done'], single = True) db = get_session() rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all() diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index e6e81740..dba0ea7e 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -146,8 +146,7 @@ class Searcher(Plugin): pre_releases = fireEvent('quality.pre_releases', single = True) release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) - available_status = fireEvent('status.get', 'available', single = True) - ignored_status = fireEvent('status.get', 'ignored', single = True) + available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True) found_releases = [] diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index f9fb5138..c496c2de 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -28,12 +28,11 @@ class StatusPlugin(Plugin): status_cached = {} def __init__(self): - addEvent('status.add', self.add) - addEvent('status.get', self.add) # Alias for .add + addEvent('status.get', self.get) addEvent('status.get_by_id', self.getById) addEvent('status.all', self.all) addEvent('app.initialize', self.fill) - addEvent('app.load', self.all) + addEvent('app.load', self.all) # Cache all statuses addApiView('status.list', self.list, docs = { 'desc': 'Check for available update', @@ -74,26 +73,35 @@ class StatusPlugin(Plugin): return temp - def add(self, identifier): + def get(self, identifiers): - if self.status_cached.get(identifier): - return self.status_cached.get(identifier) + if not isinstance(identifiers, (list)): + identifiers = [identifiers] db = get_session() + return_list = [] - s = db.query(Status).filter_by(identifier = identifier).first() - if not s: - s = Status( - identifier = identifier, - label = toUnicode(identifier.capitalize()) - ) - db.add(s) - db.commit() + for identifier in identifiers: - status_dict = s.to_dict() + if self.status_cached.get(identifier): + return_list.append(self.status_cached.get(identifier)) + continue - self.status_cached[identifier] = status_dict - return status_dict + s = db.query(Status).filter_by(identifier = identifier).first() + if not s: + s = Status( + identifier = identifier, + label = toUnicode(identifier.capitalize()) + ) + db.add(s) + db.commit() + + status_dict = s.to_dict() + + self.status_cached[identifier] = status_dict + return_list.append(status_dict) + + return return_list if len(identifiers) > 1 else return_list[0] def fill(self): diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/movie/_modifier/main.py index f0f98e0e..12d1e325 100644 --- a/couchpotato/core/providers/movie/_modifier/main.py +++ b/couchpotato/core/providers/movie/_modifier/main.py @@ -52,8 +52,7 @@ class MovieResultModifier(Plugin): if l: # Statuses - active_status = fireEvent('status.get', 'active', single = True) - done_status = fireEvent('status.get', 'done', single = True) + active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) for movie in l.movies: if movie.status_id == active_status['id']: From 367c385fff918e6ac581388259947730532cf76b Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 27 Apr 2013 11:12:15 +0200 Subject: [PATCH 088/127] Lowercase variables --- couchpotato/core/downloaders/nzbget/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index c060a334..fc54f024 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -164,9 +164,9 @@ class NZBGet(Downloader): history = rpc.history() for hist in history: if hist['Parameters'] and hist['Parameters']['couchpotato'] and hist['Parameters']['couchpotato'] == item['id']: - NZBID = hist['ID'] + nzb_id = hist['ID'] path = hist['DestDir'] - if rpc.editqueue('HistoryDelete', 0, "", [tryInt(NZBID)]): + if rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]): shutil.rmtree(path, True) except: log.error('Failed deleting: %s', traceback.format_exc(0)) From 8b0aa7a6b366194fbd8b3d1675e99347f2f0a4d8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 30 Apr 2013 00:24:56 +0200 Subject: [PATCH 089/127] Initial mobile styling --- .../notifications/core/static/notification.js | 1 + couchpotato/core/plugins/log/static/log.css | 14 +- couchpotato/core/plugins/movie/static/list.js | 124 ++-- .../core/plugins/movie/static/movie.css | 287 +++++--- .../core/plugins/movie/static/movie.js | 14 +- .../core/plugins/movie/static/search.css | 231 ++++--- .../core/plugins/movie/static/search.js | 79 +-- couchpotato/static/fonts/Elusive-Icons.eot | Bin 0 -> 41988 bytes couchpotato/static/fonts/Elusive-Icons.svg | 298 +++++++++ couchpotato/static/fonts/Elusive-Icons.ttf | Bin 0 -> 41804 bytes couchpotato/static/fonts/Elusive-Icons.woff | Bin 0 -> 76072 bytes .../static/fonts/OpenSans-Bold-webfont.eot | Bin 0 -> 21190 bytes .../static/fonts/OpenSans-Bold-webfont.svg | 146 +++++ .../static/fonts/OpenSans-Bold-webfont.ttf | Bin 0 -> 21012 bytes .../static/fonts/OpenSans-Bold-webfont.woff | Bin 0 -> 14036 bytes .../fonts/OpenSans-BoldItalic-webfont.eot | Bin 0 -> 23510 bytes .../fonts/OpenSans-BoldItalic-webfont.svg | 146 +++++ .../fonts/OpenSans-BoldItalic-webfont.ttf | Bin 0 -> 23304 bytes .../fonts/OpenSans-BoldItalic-webfont.woff | Bin 0 -> 15572 bytes .../static/fonts/OpenSans-Italic-webfont.eot | Bin 0 -> 23866 bytes .../static/fonts/OpenSans-Italic-webfont.svg | 146 +++++ .../static/fonts/OpenSans-Italic-webfont.ttf | Bin 0 -> 23680 bytes .../static/fonts/OpenSans-Italic-webfont.woff | Bin 0 -> 15836 bytes .../static/fonts/OpenSans-Regular-webfont.eot | Bin 0 -> 20878 bytes .../static/fonts/OpenSans-Regular-webfont.svg | 146 +++++ .../static/fonts/OpenSans-Regular-webfont.ttf | Bin 0 -> 20688 bytes .../fonts/OpenSans-Regular-webfont.woff | Bin 0 -> 13988 bytes couchpotato/static/images/sprite.png | Bin 1795 -> 2759 bytes couchpotato/static/scripts/block/menu.js | 2 +- .../static/scripts/block/navigation.js | 45 +- couchpotato/static/scripts/couchpotato.js | 2 +- .../static/scripts/library/question.js | 28 +- couchpotato/static/style/main.css | 615 ++++++++++++------ couchpotato/static/style/settings.css | 20 +- couchpotato/templates/index.html | 3 + 35 files changed, 1792 insertions(+), 555 deletions(-) create mode 100755 couchpotato/static/fonts/Elusive-Icons.eot create mode 100755 couchpotato/static/fonts/Elusive-Icons.svg create mode 100755 couchpotato/static/fonts/Elusive-Icons.ttf create mode 100755 couchpotato/static/fonts/Elusive-Icons.woff create mode 100755 couchpotato/static/fonts/OpenSans-Bold-webfont.eot create mode 100755 couchpotato/static/fonts/OpenSans-Bold-webfont.svg create mode 100755 couchpotato/static/fonts/OpenSans-Bold-webfont.ttf create mode 100755 couchpotato/static/fonts/OpenSans-Bold-webfont.woff create mode 100755 couchpotato/static/fonts/OpenSans-BoldItalic-webfont.eot create mode 100755 couchpotato/static/fonts/OpenSans-BoldItalic-webfont.svg create mode 100755 couchpotato/static/fonts/OpenSans-BoldItalic-webfont.ttf create mode 100755 couchpotato/static/fonts/OpenSans-BoldItalic-webfont.woff create mode 100755 couchpotato/static/fonts/OpenSans-Italic-webfont.eot create mode 100755 couchpotato/static/fonts/OpenSans-Italic-webfont.svg create mode 100755 couchpotato/static/fonts/OpenSans-Italic-webfont.ttf create mode 100755 couchpotato/static/fonts/OpenSans-Italic-webfont.woff create mode 100755 couchpotato/static/fonts/OpenSans-Regular-webfont.eot create mode 100755 couchpotato/static/fonts/OpenSans-Regular-webfont.svg create mode 100755 couchpotato/static/fonts/OpenSans-Regular-webfont.ttf create mode 100755 couchpotato/static/fonts/OpenSans-Regular-webfont.woff diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 52062e93..025d81d1 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -21,6 +21,7 @@ var NotificationBase = new Class({ App.addEvent('load', function(){ App.block.notification = new Block.Menu(self, { + 'button_class': 'icon2.eye-open', 'class': 'notification_menu', 'onOpen': self.markAsRead.bind(self) }) diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index 222b8efa..8804af63 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -5,7 +5,7 @@ margin: 0; font-size: 20px; position: fixed; - width: 960px; + width: 100%; bottom: 0; background: #4E5969; } @@ -24,7 +24,17 @@ .page.log .nav li.active { font-weight: bold; cursor: default; - font-size: 30px; + background: rgba(255,255,255,.1); + } + + @media all and (max-width: 480px) { + .page.log .nav { + font-size: 14px; + } + + .page.log .nav li { + padding: 5px; + } } .page.log .loading { diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index a89f89af..46819439 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -43,7 +43,10 @@ var MovieList = new Class({ }) : null ); - self.changeView(self.getSavedView() || self.options.view || 'details'); + if($(window).getSize().x < 480) + self.changeView('list'); + else + self.changeView(self.getSavedView() || self.options.view || 'details'); self.getMovies(); @@ -121,7 +124,7 @@ var MovieList = new Class({ if(!self.navigation_counter) return; - self.navigation_counter.set('text', (count || 0)); + self.navigation_counter.set('text', (count || 0) + ' movies'); }, @@ -146,64 +149,67 @@ var MovieList = new Class({ var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; self.current_view = self.getSavedView() || 'details'; - self.el.addClass(self.current_view+'_list') + self.el.addClass(self.current_view+'_list with_navigation') - self.navigation = new Element('div.alph_nav').adopt( - self.navigation_actions = new Element('ul.inlay.actions.reversed'), - self.navigation_counter = new Element('span.counter[title=Total]'), - self.navigation_alpha = new Element('ul.numbers', { - 'events': { - 'click:relay(li)': function(e, el){ - self.movie_list.empty() - self.activateLetter(el.get('data-letter')) - self.getMovies() + self.navigation = new Element('div.alph_nav').grab( + new Element('div').adopt( + self.navigation_alpha = new Element('ul.numbers', { + 'events': { + 'click:relay(li)': function(e, el){ + self.movie_list.empty() + self.activateLetter(el.get('data-letter')) + self.getMovies() + } } - } - }), - self.navigation_search_input = new Element('input.inlay', { - 'placeholder': 'Search', - 'events': { - 'keyup': self.search.bind(self), - 'change': self.search.bind(self) - } - }), - self.navigation_menu = new Block.Menu(self), - self.mass_edit_form = new Element('div.mass_edit_form').adopt( - new Element('span.select').adopt( - self.mass_edit_select = new Element('input[type=checkbox].inlay', { - 'events': { - 'change': self.massEditToggleAll.bind(self) - } - }), - self.mass_edit_selected = new Element('span.count', {'text': 0}), - self.mass_edit_selected_label = new Element('span', {'text': 'selected'}) - ), - new Element('div.quality').adopt( - self.mass_edit_quality = new Element('select'), - new Element('a.button.orange', { - 'text': 'Change quality', - 'events': { - 'click': self.changeQualitySelected.bind(self) - } - }) - ), - new Element('div.delete').adopt( - new Element('span[text=or]'), - new Element('a.button.red', { - 'text': 'Delete', - 'events': { - 'click': self.deleteSelected.bind(self) - } - }) - ), - new Element('div.refresh').adopt( - new Element('span[text=or]'), - new Element('a.button.green', { - 'text': 'Refresh', - 'events': { - 'click': self.refreshSelected.bind(self) - } - }) + }), + self.navigation_counter = new Element('span.counter[title=Total]'), + self.navigation_actions = new Element('ul.inlay.actions.reversed'), + self.navigation_search_input = new Element('input.search.inlay', { + 'title': 'Search through ' + self.options.identifier, + 'placeholder': 'Search through ' + self.options.identifier, + 'events': { + 'keyup': self.search.bind(self), + 'change': self.search.bind(self) + } + }), + self.navigation_menu = new Block.Menu(self), + self.mass_edit_form = new Element('div.mass_edit_form').adopt( + new Element('span.select').adopt( + self.mass_edit_select = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': self.massEditToggleAll.bind(self) + } + }), + self.mass_edit_selected = new Element('span.count', {'text': 0}), + self.mass_edit_selected_label = new Element('span', {'text': 'selected'}) + ), + new Element('div.quality').adopt( + self.mass_edit_quality = new Element('select'), + new Element('a.button.orange', { + 'text': 'Change quality', + 'events': { + 'click': self.changeQualitySelected.bind(self) + } + }) + ), + new Element('div.delete').adopt( + new Element('span[text=or]'), + new Element('a.button.red', { + 'text': 'Delete', + 'events': { + 'click': self.deleteSelected.bind(self) + } + }) + ), + new Element('div.refresh').adopt( + new Element('span[text=or]'), + new Element('a.button.green', { + 'text': 'Refresh', + 'events': { + 'click': self.refreshSelected.bind(self) + } + }) + ) ) ) ).inject(self.el, 'top'); @@ -543,7 +549,7 @@ var MovieList = new Class({ self.title[is_empty ? 'hide' : 'show']() if(self.description) - self.description[is_empty ? 'hide' : 'show']() + self.description.setStyle('display', [is_empty ? 'none' : '']) if(is_empty && self.options.on_empty_element){ self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after'); diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index c6fe4e0a..6e0f7fd3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -1,50 +1,72 @@ .movies { - padding: 60px 0 20px; + padding: 10px 0 20px; position: relative; z-index: 3; + width: 100%; } - .movies .loading { - display: block; - padding: 20px 0 0 0; - width: 100%; - z-index: 3; - transition: all .4s cubic-bezier(0.9,0,0.1,1); - height: 40px; - opacity: 1; - position: absolute; + .movies > div { + clear: both; + } + + .movies.thumbs_list > div:not(.description) { + margin-right: -4px; text-align: center; } - .movies .loading.hide { - height: 0; + + .movies .loading { + display: block; padding: 20px 0 0 0; - opacity: 0; - margin-top: -20px; - } - - .movies .loading .spinner { - display: inline-block; - } - - .movies .loading .message { - margin: 0 20px; + width: 100%; + z-index: 3; + transition: all .4s cubic-bezier(0.9,0,0.1,1); + height: 40px; + opacity: 1; + position: absolute; + text-align: center; } + .movies .loading.hide { + height: 0; + padding: 20px 0 0 0; + opacity: 0; + margin-top: -20px; + } + + .movies .loading .spinner { + display: inline-block; + } + + .movies .loading .message { + margin: 0 20px; + } - .movies > h2 { + .movies h2 { margin-bottom: 20px; } + @media all and (max-width: 480px) { + .movies h2 { + font-size: 25px; + margin-bottom: 5px; + } + } + .movies > .description { position: absolute; top: 30px; right: 0; font-style: italic; - text-shadow: none; opacity: 0.8; } .movies:hover > .description { opacity: 1; } + + @media all and (max-width: 860px) { + .movies > .description { + display: none; + } + } .movies.thumbs_list { padding: 20px 0 20px; @@ -53,21 +75,21 @@ .home .movies { padding-top: 6px; } - - - .movies.mass_edit_list { - padding-top: 90px; - } .movies .movie { position: relative; border-radius: 4px; margin: 10px 0; + padding-left: 20px; overflow: hidden; width: 100%; height: 180px; transition: all 0.6s cubic-bezier(0.9,0,0.1,1); } + + .movies.details_list .movie { + padding-left: 120px; + } .movies.list_list .movie:not(.details_view), .movies.mass_edit_list .movie { @@ -75,13 +97,22 @@ } .movies.thumbs_list .movie { - width: 153px; + width: 151px; height: 230px; display: inline-block; - margin: 0 8px 0 0; + margin: 0 8px 8px 0; + padding: 0; + vertical-align: top; } - .movies.thumbs_list .movie:nth-child(6n+6) { - margin: 0; + + @media all and (max-width: 480px) { + .movies.thumbs_list .movie { + width: 90px; + height: 137px; + display: inline-block; + margin: 0 8px 8px 0; + padding: 0; + } } .movies .movie .mask { @@ -109,8 +140,8 @@ .movies .data { padding: 20px; height: 100%; - width: 840px; - position: absolute; + width: 100%; + position: relative; right: 0; border-radius: 0; transition: all .6s cubic-bezier(0.9,0,0.1,1); @@ -119,7 +150,6 @@ .movies.mass_edit_list .movie .data { height: 30px; padding: 3px 0 3px 10px; - width: 938px; box-shadow: none; border: 0; background: #4e5969; @@ -197,27 +227,36 @@ .movies .info { position: relative; height: 100%; + width: 100%; } .movies .info .title { - display: inline; - position: absolute; font-size: 28px; font-weight: bold; margin-bottom: 10px; + margin-top: 2px; left: 0; top: 0; - width: 90%; + width: 100%; + padding-right: 80px; transition: all 0.2s linear; } + .movies .info .title span { + display: block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + height: 30px; + line-height: 30px; + top: -5px; + position: relative; + } .movies.list_list .movie:not(.details_view) .info .title, .movies.mass_edit_list .info .title { font-size: 16px; font-weight: normal; - text-overflow: ellipsis; width: auto; - overflow: hidden; - } .movies.thumbs_list .movie:not(.no_thumbnail) .info { @@ -229,25 +268,22 @@ .movies.thumbs_list .info .title { font-size: 21px; - text-shadow: 0 0 10px #000; word-wrap: break-word; + padding: 0; } .movies .info .year { position: absolute; - font-size: 30px; - margin-bottom: 10px; color: #bbb; - width: 10%; right: 0; - top: 0; + top: 1px; text-align: right; transition: all 0.2s linear; + font-weight: normal; } .movies.list_list .movie:not(.details_view) .info .year, .movies.mass_edit_list .info .year { - font-size: 16px; - width: 6%; + font-size: 1.25em; right: 10px; } @@ -259,16 +295,14 @@ top: auto; right: auto; color: #FFF; - text-shadow: none; - text-shadow: 0 0 6px #000; } .movies .info .description { - position: absolute; top: 30px; clear: both; - height: 80px; + bottom: 30px; overflow: hidden; + position: absolute; } .movies .data:hover .description { overflow: auto; @@ -281,12 +315,17 @@ .movies .data .quality { position: absolute; - bottom: 0; + bottom: 2px; display: block; min-height: 20px; - vertical-align: mid; } + @media all and (max-width: 480px) { + .movies .data .quality { + display: none; + } + } + .movies .status_suggest .data .quality, .movies.thumbs_list .data .quality { display: none; @@ -302,9 +341,8 @@ vertical-align: middle; display: inline-block; text-transform: uppercase; - text-shadow: none; font-weight: normal; - margin: 0 2px; + margin: 0 4px 0 0; border-radius: 2px; background-color: rgba(255,255,255,0.1); } @@ -312,7 +350,7 @@ .movies.mass_edit_list .data .quality { text-align: right; right: 0; - margin-right: 50px; + margin-right: 60px; z-index: 1; } @@ -342,8 +380,32 @@ bottom: 20px; right: 20px; line-height: 0; - margin-top: -25px; + top: 0; + opacity: 0; + display: none; + width: 0; } + @media all and (max-width: 480px) { + .movies .data .actions { + display: none !important; + } + } + + .movies .movie:hover .data .actions { + opacity: 1; + display: inline-block; + width: auto; + } + + .movies.details_list .data .actions { + top: auto; + bottom: 18px; + } + + .movies .movie:hover .actions { + opacity: 1; + display: inline-block; + } .movies.thumbs_list .data .actions { bottom: 8px; right: 10px; @@ -362,7 +424,6 @@ width: 26px; height: 26px; padding: 3px; - opacity: 0; } .movies.list_list .movie:not(.details_view):hover .actions, @@ -399,7 +460,6 @@ .movies .options { position: absolute; - margin-left: 120px; width: 840px; } @@ -423,7 +483,6 @@ .movies .options .table .item.ignored span { text-decoration: line-through; color: rgba(255,255,255,0.4); - text-shadow: none; } .movies .options .table .item.ignored .delete { background-image: url('../images/icon.undo.png'); @@ -491,6 +550,9 @@ text-align: center; transition: all .6s cubic-bezier(0.9,0,0.1,1); overflow: hidden; + left: 0; + position: absolute; + z-index: 10; } .movies .movie .trailer_container.hide { height: 0 !important; @@ -507,6 +569,7 @@ background: #4e5969; border-radius: 0 0 2px 2px; transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s; + z-index: 11; } .movies .movie .hide_trailer.hide { top: -30px; @@ -543,11 +606,13 @@ right: 135px; z-index: 2; opacity: 0; - text-shadow: none; background: #4e5969; min-width: 300px; text-align: right; + height: 100%; + padding: 3px 0; } + .movies.mass_edit_list .trynext { display: none; } .wanted .movies .movie .trynext { padding-right: 50px; } @@ -555,6 +620,14 @@ opacity: 1; } + .movies.details_list .movie .trynext { + background: #47515f; + padding: 0; + right: 0; + bottom: 35px; + height: auto; + } + .movies .movie .trynext a { background-position: 5px center; padding: 0 5px 0 25px; @@ -562,6 +635,9 @@ color: #FFF; border-radius: 2px; } + .movies .movie .trynext a:last-child { + margin: 0; + } .movies .movie .trynext a:hover { background-color: #369545; } @@ -578,24 +654,33 @@ .movies .alph_nav { transition: box-shadow .4s linear; - position: fixed; + position: relative; z-index: 4; - top: 0; - padding: 100px 60px 7px; - width: 1080px; - margin: 0 -60px; - box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1); - background: #4e5969; + top: 0px; + height: 45px; + right: 0; + margin: 0 auto; + width: 100%; + padding: 10px 0; } - .movies .alph_nav.float { - box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5); - border-radius: 0; + @media all and (max-width: 480px) { + .movies .alph_nav { + display: none; + } } -.movies .alph_nav ul.numbers, + .movies .alph_nav > div { + position: relative; + max-width: 980px; + height: 45px; + margin: 0 auto; + padding: 0; + } + +.movies .alph_nav .numbers, .movies .alph_nav .counter, -.movies .alph_nav ul.actions { +.movies .alph_nav .actions { list-style: none; padding: 0 0 1px; margin: 0; @@ -604,49 +689,62 @@ } .movies .alph_nav .counter { - width: 60px; - text-align: center; + text-align: right; + position: absolute; + right: 270px; + background: #4e5969; + padding: 4px 10px; } .movies .alph_nav .numbers li, .movies .alph_nav .actions li { display: inline-block; vertical-align: top; - width: 20px; height: 24px; - line-height: 26px; + line-height: 23px; text-align: center; cursor: pointer; color: rgba(255,255,255,0.2); border: 1px solid transparent; transition: all 0.1s ease-in-out; - text-shadow: none; } - .movies .alph_nav .numbers li:first-child { - width: 43px; - } + + @media all and (max-width: 900px) { + .movies .alph_nav .numbers { + display: none; + } + } + + .movies .alph_nav .numbers li { + width: auto; + padding: 0 4px; + } + .movies .alph_nav li.available { - color: rgba(255,255,255,0.8); + color: #FFF; font-weight: bolder; } - .movies .alph_nav li.active.available, .movies .alph_nav li.available:hover { - color: #fff; - font-size: 20px; - line-height: 20px; + .movies .alph_nav li.active.available, + .movies .alph_nav li.available:hover { + background: rgba(255,255,255,.1); } - .movies .alph_nav input { + .movies .alph_nav .search { padding: 6px 5px; - margin: 0 0 0 6px; - float: left; - width: 155px; + margin: 0 0 0 20px; + position: absolute; + right: 30px; + width: 154px; height: 25px; + transition: all 0.6s cubic-bezier(0.9,0,0.1,1); } .movies .alph_nav .actions { margin: 0 6px 0 0; -moz-user-select: none; + position: absolute; + right: 183px; } .movies .alph_nav .actions li { border-radius: 1px; @@ -730,10 +828,12 @@ } .movies .alph_nav .more_menu { - margin-left: 48px; + right: 0; + position: absolute; } .movies .alph_nav .more_menu > a { + background-color: #4e5969; background-position: center -158px; } @@ -790,7 +890,6 @@ font-weight: bold; display: inline-block; text-transform: uppercase; - text-shadow: none; font-weight: normal; font-size: 20px; border-left: 1px solid rgba(255, 255, 255, .2); diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index ffdd14d1..4f903226 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -126,12 +126,14 @@ var Movie = new Class({ self.thumbnail = File.Select.single('poster', self.data.library.files), self.data_container = new Element('div.data.inlay.light').adopt( self.info_container = new Element('div.info').adopt( - self.title = new Element('div.title', { - 'text': self.getTitle() || 'n/a' - }), - self.year = new Element('div.year', { - 'text': self.data.library.year || 'n/a' - }), + new Element('div.title').adopt( + self.title = new Element('span', { + 'text': self.getTitle() || 'n/a' + }), + self.year = new Element('div.year', { + 'text': self.data.library.year || 'n/a' + }) + ), self.rating = new Element('div.rating.icon', { 'text': self.data.library.rating }), diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index 23d87b46..e03c6461 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -1,105 +1,143 @@ .search_form { display: inline-block; vertical-align: middle; - width: 25%; + position: absolute; + right: 105px; + top: 0; + text-align: right; + height: 100%; + border-bottom: 4px solid transparent; + transition: all .4s cubic-bezier(0.9,0,0.1,1); + position: absolute; + z-index: 20; + border: 1px solid transparent; + border-width: 0 0 4px; } - - .search_form input { - padding: 4px 20px 4px 4px; - margin: 0; - font-size: 14px; - width: 100%; - height: 24px; + .search_form:hover { + border-color: #047792; } - .search_form input:focus { - padding-right: 83px; + + @media all and (max-width: 480px) { + .search_form { + right: 44px; + } + } + + .search_form.focused, + .search_form.shown { + border-color: #04bce6; + } + + .search_form .input { + height: 100%; + overflow: hidden; + width: 45px; + transition: all .4s cubic-bezier(0.9,0,0.1,1); + } + + .search_form.focused .input, + .search_form.shown .input { + width: 380px; + background: #4e5969; } - .search_form .input .enter { - background: #369545 url('../images/sprite.png') right -188px no-repeat; - padding: 0 20px 0 4px; - border-radius: 2px; - text-transform: uppercase; - font-size: 10px; - margin-left: -78px; - display: inline-block; - opacity: 0; - position: relative; - top: -2px; - cursor: pointer; - vertical-align: middle; - visibility: hidden; - } - .search_form.focused .input .enter { - visibility: visible; + .search_form .input input { + border-radius: 0; + display: block; + width: 100%; + border: 0; + background: rgba(255,255,255,.08); + color: #FFF; + font-size: 25px; + height: 100%; + padding: 10px; + width: 100%; + opacity: 0; + padding: 0 40px 0 10px; + transition: all .4s ease-in-out .2s; } - .search_form.focused.filled .input .enter { - opacity: 1; + .search_form.focused .input input, + .search_form.shown .input input { + opacity: 1; + } + + @media all and (max-width: 480px) { + .search_form .input input { + font-size: 15px; + } + + .search_form.focused .input, + .search_form.shown .input { + width: 277px; + } } - - .search_form .input a { - width: 17px; - height: 20px; - display: inline-block; - margin: -2px 0 0 2px; - top: 4px; - right: 5px; - background: url('../images/sprite.png') left -37px no-repeat; - cursor: pointer; - opacity: 0; - transition: all 0.2s ease-in-out; - vertical-align: middle; + + .search_form .input a { + position: absolute; + top: 0; + right: 0; + width: 44px; + height: 100%; + cursor: pointer; + vertical-align: middle; + text-align: center; + line-height: 66px; + font-size: 15px; + color: #FFF; + } + + .search_form .input a:after { + content: "\e03e"; } - .search_form.filled .input a { - opacity: 1; + .search_form.shown.filled .input a:after { + content: "\e04e"; + } + + @media all and (max-width: 480px) { + .search_form .input a { + line-height: 44px; + } } .search_form .results_container { + text-align: left; position: absolute; background: #5c697b; - margin: 6px 0 0 -230px; + margin: 4px 0 0; width: 470px; min-height: 140px; - border-radius: 3px; box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55); display: none; } + @media all and (max-width: 480px) { + .search_form .results_container { + width: 320px; + } + } + .search_form.focused.filled .results_container, .search_form.shown.filled .results_container { display: block; } - .search_form .results_container:before { - content: ' '; - height: 0; - position: relative; - width: 0; - border: 10px solid transparent; - border-bottom-color: #5c697b; - display: block; - top: -20px; - left: 346px; - } - .search_form .results { max-height: 570px; overflow-x: hidden; - padding: 10px 0; - margin-top: -18px; } .movie_result { overflow: hidden; - height: 140px; + height: 50px; position: relative; } .movie_result .options { position: absolute; height: 100%; - width: 100%; top: 0; - left: 0; + left: 30px; + right: 0; + padding: 13px; border: 1px solid transparent; border-width: 1px 0; border-radius: 0; @@ -107,7 +145,6 @@ } .movie_result .options > div { - padding: 0 15px; border: 0; } @@ -122,6 +159,13 @@ } .movie_result .options select[name=title] { width: 180px; } .movie_result .options select[name=profile] { width: 90px; } + + @media all and (max-width: 480px) { + + .movie_result .options select[name=title] { width: 90px; } + .movie_result .options select[name=profile] { width: 60px; } + + } .movie_result .options .button { vertical-align: middle; @@ -130,25 +174,21 @@ .movie_result .options .message { height: 100%; - line-height: 140px; font-size: 20px; - text-align: center; color: #fff; + line-height: 20px; } .movie_result .data { - padding: 0 15px; position: absolute; height: 100%; - width: 100%; top: 0; - left: 0; + left: 30px; + right: 0; background: #5c697b; cursor: pointer; - - border-bottom: 1px solid #333; - border-top: 1px solid rgba(255,255,255, 0.15); - transition: all .6s cubic-bezier(0.9,0,0.1,1); + border-top: 1px solid rgba(255,255,255, 0.08); + transition: all .4s cubic-bezier(0.9,0,0.1,1); } .movie_result .data.open { left: 100%; @@ -162,49 +202,40 @@ } .movie_result .thumbnail { - width: 17%; - display: inline-block; - margin: 15px 3% 15px 0; + width: 34px; + min-height: 100%; + display: block; + margin: 0; vertical-align: top; - border-radius: 3px; - box-shadow: 0 0 3px rgba(0,0,0,0.35); } .movie_result .info { - width: 80%; - display: inline-block; - vertical-align: top; - padding: 15px 0; - height: 120px; - overflow: hidden; - } - - .movie_result .info .tagline { - max-height: 70px; - overflow: hidden; - display: inline-block; - } - - .movie_result .add +.info { - margin-left: 20%; + position: absolute; + top: 20%; + left: 15px; + right: 60px; + vertical-align: middle; } .movie_result .info h2 { + font-weight: normal; + font-size: 20px; + display: block; margin: 0; - font-size: 17px; - line-height: 20px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; } .movie_result .info h2 span { padding: 0 5px; + position: absolute; + right: -60px; } - .movie_result .info h2 span:before { content: "("; } - .movie_result .info h2 span:after { content: ")"; } - .search_form .mask, .movie_result .mask { - border-radius: 3px; position: absolute; height: 100%; width: 100%; diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index dd8e7b04..334f8554 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -7,33 +7,30 @@ Block.Search = new Class({ create: function(){ var self = this; + var focus_timer = 0; self.el = new Element('div.search_form').adopt( new Element('div.input').adopt( - self.input = new Element('input.inlay', { + self.input = new Element('input', { 'placeholder': 'Search & add a new movie', 'events': { 'keyup': self.keyup.bind(self), 'focus': function(){ + if(focus_timer) clearTimeout(focus_timer); self.el.addClass('focused') if(this.get('value')) self.hideResults(false) }, 'blur': function(){ - (function(){ + focus_timer = (function(){ self.el.removeClass('focused') - }).delay(2000); + }).delay(100); } } }), - new Element('span.enter', { + new Element('a.icon2', { 'events': { - 'click': self.keyup.bind(self) - }, - 'text':'Enter' - }), - new Element('a', { - 'events': { - 'click': self.clear.bind(self) + 'click': self.clear.bind(self), + 'touchend': self.clear.bind(self) } }) ), @@ -59,13 +56,21 @@ Block.Search = new Class({ var self = this; (e).preventDefault(); - self.last_q = ''; - self.input.set('value', ''); - self.input.focus() + if(self.last_q === ''){ + self.input.blur() + self.last_q = null; + } + else { - self.movies = [] - self.results.empty() - self.el.removeClass('filled') + self.last_q = ''; + self.input.set('value', ''); + self.input.focus() + + self.movies = [] + self.results.empty() + self.el.removeClass('filled') + + } }, hideResults: function(bool){ @@ -92,8 +97,10 @@ Block.Search = new Class({ self.el[self.q() ? 'addClass' : 'removeClass']('filled') - if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click')) - self.autocomplete() + if(self.q() != self.last_q){ + if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer) + self.autocomplete_timer = self.autocomplete.delay(300, self) + } }, @@ -197,6 +204,11 @@ Block.Search.Item = new Class({ self.el = new Element('div.movie_result', { 'id': info.imdb }).adopt( + self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { + 'src': info.images.poster[0], + 'height': null, + 'width': null + }) : null, self.options_el = new Element('div.options.inlay'), self.data_container = new Element('div.data', { 'tween': { @@ -207,11 +219,6 @@ Block.Search.Item = new Class({ 'click': self.showOptions.bind(self) } }).adopt( - self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { - 'src': info.images.poster[0], - 'height': null, - 'width': null - }) : null, new Element('div.info').adopt( self.title = new Element('h2', { 'text': info.titles[0] @@ -219,28 +226,11 @@ Block.Search.Item = new Class({ self.year = info.year ? new Element('span.year', { 'text': info.year }) : null - ), - self.tagline = new Element('span.tagline', { - 'text': info.tagline ? info.tagline : info.plot, - 'title': info.tagline ? info.tagline : info.plot - }), - self.director = self.info.director ? new Element('span.director', { - 'text': 'Director:' + info.director - }) : null, - self.starring = info.actors ? new Element('span.actors', { - 'text': 'Starring:' - }) : null + ) ) ) ) - if(info.actors){ - Object.each(info.actors, function(actor){ - new Element('span', { - 'text': actor - }).inject(self.starring) - }) - } info.titles.each(function(title){ self.alternativeTitle({ @@ -320,11 +310,6 @@ Block.Search.Item = new Class({ self.options_el.grab( new Element('div').adopt( - self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', { - 'src': info.images.poster[0], - 'height': null, - 'width': null - }) : null, self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', { 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label }) : (in_library ? new Element('span.in_library', { diff --git a/couchpotato/static/fonts/Elusive-Icons.eot b/couchpotato/static/fonts/Elusive-Icons.eot new file mode 100755 index 0000000000000000000000000000000000000000..282e60896c89e5e148bfcabd7598bb19288e7179 GIT binary patch literal 41988 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 literal 0 HcmV?d00001 diff --git a/couchpotato/static/fonts/Elusive-Icons.svg b/couchpotato/static/fonts/Elusive-Icons.svg new file mode 100755 index 00000000..bfc6c9b7 --- /dev/null +++ b/couchpotato/static/fonts/Elusive-Icons.svg @@ -0,0 +1,298 @@ + + + + +This is a custom SVG font generated by IcoMoon. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/couchpotato/static/fonts/Elusive-Icons.ttf b/couchpotato/static/fonts/Elusive-Icons.ttf new file mode 100755 index 0000000000000000000000000000000000000000..ae31768fb97f214499b6bc2445cd84d7e0b34e35 GIT binary patch literal 41804 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 J3jhFp00dF8bEyCT literal 0 HcmV?d00001 diff --git a/couchpotato/static/fonts/OpenSans-Bold-webfont.eot b/couchpotato/static/fonts/OpenSans-Bold-webfont.eot new file mode 100755 index 0000000000000000000000000000000000000000..e1c7674430d0dc4eb95a461948412adcd3194385 GIT binary patch literal 21190 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 + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Digitized data copyright 20102011 Google Corporation +Foundry : Ascender Corporation +Foundry URL : httpwwwascendercorpcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/couchpotato/static/fonts/OpenSans-Bold-webfont.ttf b/couchpotato/static/fonts/OpenSans-Bold-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..2d94f0629db751b8412a814df98fbe82f6fdd1df GIT binary patch literal 21012 zcmbt+3w#vS+4ng!v$NN1_L|M+x|>Z1F$p2Ngft)!5CH>3h!_J#J=fGeE;XnZlwL%?|pyYOPHO@&dhnv zbDr~D{?BukamJVj9|~h5Mobuc^&?OJoiXmh)q;`3FCM{?OvZU{oJTIbtR}v3?rj(1 zd=$>7OrJY-{+TPQ=zJ@-7pLE_IAS+k!x@to6*~~M48v6jwZHx_{GHb@vj?Ni7Z)2=!7QXL43m39emOtZs1I|ll&0TzB zMRUb!oIi{A1+JSned-IJyz>QPWB!WvU2~`2IG=Cizh-Rghqym-{nWWLN^2WIUo(2g z_s*ZUXmP^b=Qv}R?!)=Z^B2yT|7l;})r^h57x&+Vjkv*HxO-kL?O#&2wx;)!#5Zo7 z<+Z(=5!|;?-1l4D$0D9c zZlvH}@9X}c`l=#-=FM<*T~f3)Pi!|zRv5EuT3|2PId zb2BHd(RThnd?_my)oQaloUSZ)w#V!92ZA}F+;CogL19sGBw7+HE$da@yW)b%s_H&9 zeQV>1es%rpf6y>s;Dv(*4{01a>>^0*$WfOxjUF?$`ODQZ|L0?jvKU!#;P31k zX7F14^=E%!U$IVJ#e4BAwnk%B8uO3VE*RIQHQg{#W3eH*np)X5Xrj2bVq)Yk8u$0f zt=4!|gxVEU)II&t&s_4;ZEP7MhyZNt7%*TCg zUHP9+%#TGiOJ$oj;)aRhi-{BQK1)?r`=n}3t

QU1&XWS9^QD#_$?zm0l_?HTGPx zRr$P;hMH>4UKLqEjXaC@M>M(Yl2}AjdNpZmT-(|iYo|u&;0O89=*0ZB;;h9yqXr!& zb)I}r6pc8mB7YX$aaBcXG;3vhTO=|vHe%|WNL!?1n)wR4FN<16(~-52k!wdxjjfHW zjfv)B8f$6O8gVE3N6l)DGw1}*xJ65Y4(CRr`H{nG!3I2Z323`KO&jRLZP`_^$l-M3 zu}ItK%krZdpV+n*9l9j8HWpcX$=cXdqJ+9bpG1#`hQWtn@KOip!1pg50HLuFv8i*W zoZl0A)L(^8tX)HFYwC!twQ7-ZZ5QV6!#9Ddove{J4jaZt@Ak0iOnlPKmxJ=i+LpH1 zG*BNKmX95e4Fl&JTiTK=!Y-OVEXgArJ1sI@3(qJJl;VxSDh*e`7kpHch;qiE4YDy4 z_d_Zj&^I!(vJ0X2Ijc-57UohalU&BJRUC88R->`QDaFQ|N~NZFbPKOc$>L16^GepY zHsOtW%c9=se)*EFVrf~|8q2P;O|z7b(8iMPWBd)Y;Y5p9vZM>|)$BDXSz&&ql4~A% zwdRVaR0k`<+p{u9JKZX?Ri?6SEEkuvYf^4;qE(~gu_cpQFT1k6WkS2cCndgGH=(ty<59kWT!RGX9ZrQ!D#IQ<2UG^d zSu&VIcL)9U%AK}g4j5}S@3PjUY~~$S8{J`5?3G&3qdW1aOMIb6d8O7rZ_nU8Uyf$M zO2?kTN51@u4zxVaPASjouiPn%PnABkuxF<|Y{Nm0XQv~_iG!eLXI9XK+dSgaD?S7C zN%w@rJ$NctJcW1WXWmngc~2qTv$H63XE9xu8$FUtyzqF5X=Ez;{L4SBF^|ThE?OU} zi~19Cg0^7P8VzE2{4s20iTYSI57u<@=-4A;pKE@r`LuDohD*j>3w9 zT6>jyizLhtYcN(v$2{aM@59RiIiWh0_pj^MtGw-lm+n6H@G47-yyCS*hSus>oCsHN-5Z z*O#TGFfPK0zA14;4hv5WV^h7h- zr9P08h}ZRtm8gE6u=9WS#SlL6*H0w3Z%%bq$EQq*SC5k`Pj|k_pZ%B1io{>|{GE?J z{`_yZuIN0w{M<@Xn|u#lJofSD?~04$0k(9XP#O4F2pOwoD_Am@c$ZJSiz>&Hc4Au% zF(^(9(u*9&wIYvJ9y#pQRmhZDqnBe>u$|6*`@==SNGkNoZW& zq$eB`+=%AlhH!&c1EEq~If5&uIQ8Lm8EtFj{p+KuWaZIvXx>r?QzBl^-P|7t4aQx) zz@?UD4~`yw{J@^~);`J$M)u=_OK%y+tBo~_p8m&I#^3qSl{0RcVWb9J)OvMm`>2MC z#`1ga-an!BXIHJ?^VWWnmA@3hVw#G9(7NGsar^u20u-JnLY zXVB&RF`>&#OikXSx0M>LM{kqaO6zB#n@KB^d~()e?3I?``7CJVqzrCy%`HR?O0o

@%hI6 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 GmH!73$*E-k literal 0 HcmV?d00001 diff --git a/couchpotato/static/fonts/OpenSans-Bold-webfont.woff b/couchpotato/static/fonts/OpenSans-Bold-webfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..cd86852d0a0c1930dab144b687318df762aa7ad3 GIT binary patch literal 14036 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 literal 0 HcmV?d00001 diff --git a/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.svg b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100755 index 00000000..8392240a --- /dev/null +++ b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,146 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Digitized data copyright 20102011 Google Corporation +Foundry : Ascender Corporation +Foundry URL : httpwwwascendercorpcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.ttf b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..f74e0e3ca7fc8276e5ad7adf934032d4f6f8f7b6 GIT binary patch literal 23304 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 Jqg8+5{{i(PmP-Ht literal 0 HcmV?d00001 diff --git a/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.woff b/couchpotato/static/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..f3248c1142bc295baf37546c887f63cb021bdf00 GIT binary patch literal 15572 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

3m6 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 literal 0 HcmV?d00001 diff --git a/couchpotato/static/fonts/OpenSans-Italic-webfont.svg b/couchpotato/static/fonts/OpenSans-Italic-webfont.svg new file mode 100755 index 00000000..29c7497f --- /dev/null +++ b/couchpotato/static/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,146 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Digitized data copyright 20102011 Google Corporation +Foundry : Ascender Corporation +Foundry URL : httpwwwascendercorpcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/couchpotato/static/fonts/OpenSans-Italic-webfont.ttf b/couchpotato/static/fonts/OpenSans-Italic-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..63f187e984b225756e2030556c4465eeaf8724e5 GIT binary patch literal 23680 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 literal 0 HcmV?d00001 diff --git a/couchpotato/static/fonts/OpenSans-Regular-webfont.svg b/couchpotato/static/fonts/OpenSans-Regular-webfont.svg new file mode 100755 index 00000000..01038bb1 --- /dev/null +++ b/couchpotato/static/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,146 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Digitized data copyright 20102011 Google Corporation +Foundry : Ascender Corporation +Foundry URL : httpwwwascendercorpcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/couchpotato/static/fonts/OpenSans-Regular-webfont.ttf b/couchpotato/static/fonts/OpenSans-Regular-webfont.ttf new file mode 100755 index 0000000000000000000000000000000000000000..05951e7b36b2352b1a69e2052d08fbffd77d5151 GIT binary patch literal 20688 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 literal 0 HcmV?d00001 diff --git a/couchpotato/static/fonts/OpenSans-Regular-webfont.woff b/couchpotato/static/fonts/OpenSans-Regular-webfont.woff new file mode 100755 index 0000000000000000000000000000000000000000..274664b28e8ed522cf1753f933ca085d8c8cc330 GIT binary patch literal 13988 zcmY*=b8sfj8|@q0&c?QF+qUy28*gme*jO8EoQ-YUwrxAP`F{8QbGz!)^f`T=erjr_ zYpS}RaZ{9#0Du6#8%H4k`M;~u^#A4m_5c4SA)zJ@0D#zht3SShXPDATB`GE@@vVh_ zbNp`*0crtYimHq(-&)Bx*Z+o59)~ZssjaaC000X8tz&&-^jVjV&(zhK2mk>420-XP zzycA#lj#STBY+>y59Sec}?fH)XACN6RC9sod_0RV_VYYC@y zS(+Q00RT+R-yYU)aJ(y2_F8_6-`e>%C;SE(QVztTrLD6&0Kg3Yog?Tw2Rh0kGlGr1 z>9>z1{@e5Zt;6S6)W6yqyMO0pjsDh%{sTw}NV}b}t@*cB@y#E;eVyc7Ldp*IPR;-T zTj4jS`o=gbBEyM;qxp9%`}ntS`CIcIHo~NHUo|x{GBN`MCS|{b61}pm&B{>e!vO^H zAQl1tjhPu4ZG)M>Kq4EN0B&^s$>7q4005X1K;!o$|BVBUjkCJDN4vXuq88Eu0+b0v zSPjjjOy7rl`uisa2ggTy`}=SG15P@?f<)NBz=9x=S+RK_d07Dv1WeTKs=D-&^cv}B@+#t~SfkCCA zYJX$qK9$g9_%rNADhKLtDk+*_8Y=1%>SNk|+6rYg&+h9lL8T5*r45qm)&?OLcvw(a zR9Ked(@()qzvHi-FR0I}kFK+?MJR0~6+$w8DR<;MqFdo@u)l9hmy|U1LK~vsMrNk| zv_LaZRJmhVe*-gPbJL^k{oTWzgFP%{bW}_f47Akv--#&+$w`_@YAPDa>Z(iA^RtUH z3v--IY%Cni?5s`I^|g&P4Rvk~&W^6XT%4Y+?{6P(9`2B!;b9SB5a8m%qa$M@VxknK z$a<4UsJofWhNY}0gChz}R5fzJi)d)9t;)-K#wkpC0f3S-nP9-@_ge=p1xNPyQVx2whP0$oEtC(6 zha(dRPz?W_&K{A^#>-phG?~sSJDO1D_Os)t)Cp5+$1Vt%=}TF-D((+8`w$}`vD!mQ z&%x?>(CX6_*8^lbP{0-Pn9>~;m6%ibgL%>7$08WCBk(oG2Nje8%*fHPAm$J5_GY#F zdCgv+?e-yTSi_@-02zutr?&nuq&E7Ma;RsE)5yV^;pRY0BA5ngX;m2`G?&X$=qV6u z0^|5%Q-MHHR3SJb=3vaCzo16X+Ue?$drrr*TqxtT4T4y$o)Z|hR{#@oPanY|D;gZM zOv5bFPZlPk!0lEmNUbE07|d*+I6K*FTp93dpPi{>Vh*o!9H_dc8^+{|B6+>RIGBhi zP@Bgg5xg9$1#1MF5U;G>nK$Uf7zNTY|H+C>ZshTLVMqyD$;*MlXl|Qr8jTT{v&FIX zVm`1@rgtQs*XE|qu6FYKP?XVPYY#;+E@bCV&Gy8Rk6(e%1wzyhgbc8r_|2HjuNfVe ze)UZ_!WDb=x*E*ywEzGCfP8%ckla@Xygb~|)fe41bUY2qJSTG4y0*Q!>HN~Q_Ni|$Dpu>mrK}m@6g$IpqW2hA3;Dg3WgkEp6 zoyvJ4xUTLd?%MslO^VB#yrx{IUb44325!~~V$p0>z$hJn|7fxHpfw94io~L!9gI21 z!>y>5TO{Ovqm@xvqf*a;bLVMGqbxPBl?DHPk4If4)k_^eAXJ=8#brRuAYMJpZ@RCoe6t)PR;@zVZ@zQoa^Y!(Br~9`xH5}f!eD9;!pOJgWZ(ej z!lBXJ=m)f+`3^6R9T;)y3--uLi9K>sla7AHQZ3(Fq>(0#k#joL0G3$}6{o@_3Yb~P zvFeub9q^B&y3gs#j*=_RAA3p$wb33rjdXx-Mq~^T=cK}uBLi_{&%b`c|~Wp+TIaqD<-sMSEiViM*7-t-EF?^ON8BS9|rDAjMlF1OFOcb#43d;7U#ze zPeVVmx`rDImaqhUW3a43^`r=)#Yv9_>Yhui&SMqzb`X?ZD|o+H2J1#m21 zYvVb=<4U>-K9RkGJDyAY>v@IBr>E#h{%;eF`_b>jeH)d}>3zeOXMFVZrkcwh;O_BtKek?Pby90k)Wg@XbVcVb0O@y}R zNC8{;d;=Et zXw%Vge@7FkL!;p$#*TKe!jSWMuc@5i_5Ma#^)f>WG8gYf?V|M^wYJiJgVl-C3wL+6 z!O3g+L_UGnV)HW^^W95TtEAJ|S&~KbQ`E_2GDDb@*D0%CzSVniUYw_YgE4sICSBdXIgE)RuK&#Y+!5^#V+yS?Jo6TC%Jvl*h8b|OJNSl); z%B-#tSlPZYs+-~+!iXG0<=C7(BAc(_ctB~%@N6?1X)e1|>1#Z*Y5}!X`*V&y$O2Y; zEK`t@Qj<>iefAU!v&w+U{rNgiGz^K5?zVU;>+SZezolqZhb0Ss&HE?UA#3f$efrg> zC(p0TD*N`&)lB>LSDL7V($p!Lp}&243`+^LdqAfe=Xy_u* zh55`orY;v)ig`Zmvq&5ShV0tIK6SVqWsoU-V!p=J;B$un__A^Z@rf!mpPV$*alnIm zSAJ2DN51jETz7hQPjn!|H9Igl(xj8l9E9~moF>0LJs3`(%sdu$cb7TQ6J>)z6>HG` zV&(IriR028$gjQfk+FUF2`4qfxkmU>`r0pewy5Frh1%gedOWEfWLMt$mV-iC9G~ul zDA(@vzCU_8QB~P&e@1w`*J02W<@*52kCDmuTK5E`(-P%)BIHgG^u4H9tIG8mi`g4= zRFWsUO5CKoIy}{0l4=pdP<_FSicHX9t~8MtaFJ>@cBUi@+~R3ri-jNz81-LdE2d;? z`XpUZMPctzxdHEkc&~04;}Gb&4+;5^Qne!+k&L%e8z$k)qd2xzNltib`WVcNU&`Dv zpXN&CP-!XwMj(DTvE!(y&DnW0cwoKAuvUE?@>iu#xBj<*zn`Y?AeG02tI9Ecg8U)Y1wBa=>+!nb**wB2l1W!6wHa|X^IziBOKee! zxl?8{Ks0j%FTE@JQx7iQV5we&Rjd{GZ47!qV)RRBopzK-aGxxR(dn<>C5;lvah4id z+9qVch8+tHR__Vmfb0*85ac;_8aQgD@(*J23ON#M%tROGUXjlh3u%1(Pda>ID;Ivx zIl~dKb?!+}Zh)DPqV|lc#vwJ#X*Gg~8Y+>kwiYuT)E<<4dy5;;&?1P})Eno+1vMfX zQQ=P)AN(EyCFRTJyIQp)FCLPWB`D#sXACJaOgfkE=!ZG+m%}YScZ(IqXzw-H6WDS= z8W@NYCIE2|ZI0kNNO}nAH7U@u%;G@Z=S^LrD&2dcVaZ_5O~_tfkr4fiQCrSY z8I@c>KrP9p_b(#5_wGTR36|E9E5q=^k%@-hw4J2Ll#eeIbZ(IaH|Llm!MJ0!>)g%? z*o#~pY})FFT{GJ+I^&TXOYlAF^;ml^{+o5hBj1;$DGR?%y^d1gKxBse!aMaXnMay{ zbn1-9y*A5_e^1k@h4J_PelPVABb{hwg8dHa#8_y|N647{kErT4^T9?UPDEm8D79%W z!=Mf170%@jD0d1hZZi^5U27J*A}Y?ms*lrIHw^WL2D1UnQ zuG2gPV(jcz1a2gU%*M_hv&7SK1nq3bf6N1K8i=6sglW{vRqP$P`03ci$D|kyrnnA8 zHrV6S-ig1!RsIUh``jeCwX^Iz#!ceC47EzMW(sx%kH=Jcex~5S{Ucy;xw}~w@%%(< z-Kq^8;ezx|f?>gQL~GF!5G{KYqeC!PJCA6d$jF8KI~=NT+TCvprQmqLPNPFHd@(9! ztgnIGND51PSa%E8YgLH9uaPM^HWFeX9`W*?Ua?%=n^ioV=b$ai&5ZR(B9ZcNgxsYF z-$0A)bQDozMuC6#UdxF{=jUj10oEt!!+CU9cb2PZePVdhReW`FVgxx zwgJh`$uybJy^&U0c;ObcJ( zK7fv5*TA|J&3k(wek7P~u>``%#q4Cb?kNn5ld1cJ&b2glBay;F-=TEWPQi(4V4!dl z1be#MOAIcJI9T5f^7pK2P9%!gp9R)!RPOmny<>4A=sfaQAHC*{c97il%F6<{OYfgz z5Pl>tuAlW`UE~_8Vtywzd9w9D{s=S8YnSp9mmd4a+#+-vK`>{UfM!^Six#hzG^B>n z`XAPtCbIox?M&RLzch!#EQib?9&WV>phd!0NUf9>9g%0dUupf1vWT{^qPB01Q8dvf z$npsCmic9c86XL*lVzcS_PR`ATjVi>Upd$e; zpPjn7`=iyou^~ExE}N5*(}Z`mTTG9>HnMDAX~7EKr<^ zfyJ)z$*L)%05ENb_DN_r8Rv39EzGDD_EHY`l1c<7(4JUIg0>X@hLkrAE*RQ7QdkrJ<9$BmU?twN z1$~#peeSFN$N6y^p-*&Q12@k7;5v42t9z9)n%Lil~5Z+cm#=l2kr z0=efKunp2FoKgj~XYREeXYW5nDRaJ>Hk4^RC$iNbn?o-x2KTqze5Phw>J8B}>=Ofqht`Ru{iO z^$b+uDrRU*A00O)Q|%R)#icQ10oR2?W6f+}z?lnUDlBJ}~5v+yA=id<65b`l+E=6k0 z&Hj0bmR6{6(!!`lcsL~h&)OzgA&cIyw31Hms1%buY?Iu4tGEuOyt>q7w|r*>JDS8( z;kSzsZkI~BTzwgIQN8qZ=@)j!?aoWu%{8!Hzw7em(#xvQ@!*E!tei zQ;12i99RT@T|-%D)&0XCS;8+{@7zp7HzPLQ;Iphl16rtCSciA;8LY!#ZJZ4`^V;jw ztao!?!y{(zY5Z%+5~(pivLJpzhE}~Q-?ehy(>1Jdq;~|wtr;54sWl&X0$u4tMdGRC zGF_!)2sGaSltTmtZVe^EZwag`?0=-y(Y_Zs5J+x+$OR61v?tJ!ti!PEU#xM;;ogLA z%o-2$pDs;ZUK+aDzP9rc_y&B+TIVC^N(rjmo@loD<SZ-ZRwk8o_0KRe!^U(|m#tC3OLiSzzUn;pK< z?|79XjD$XWIM#BYBRcEL~n^6RC z2&f*vD^VdQ#kmuMY!Vxw480Te`~{zUf+fCBq>Th2tJfO8F6GB9c_vk=@%6~-pEbA3 zSk}fUgP`j=>A}CDA>HNu#VCtK=XF?mR4l-M2NLgtx&Kg5k@LW2mHvaNYM6;}mA-`Z zBRV)G^w8-|NB}8_I7_=Hgw#_FZEQa0aYkwyzNed-#mtsp1lXbA>G!M1PDy^^iA^{Q zOQWl+ETU7^@KDi5Jk@A6VFwVqpFt46W0{s0WB zN1B)hQ>pr(uVj)C7b9#j(Y$Or++S!6iHDu69N;pfmA8;NFxc#^iY(c=WwSZzDRY= zxCrT+?x%4b`h4|%378)ln%Ykp~HtH!1x zZy0Eea!^U+XtYVP_buI5DvBwlgse;zYb&Qu%}c2aB%uQ@wJOB%`SpOwL3D{K8iO?E zwe1>{e0F?Nl0TL}G|0Li%+`NY?mUP)@uy)#mP-!5kXc%jNHAc*>p|0@b`NbG-;KBwJI`NYYG%-C#L2SR7Vk+1H{XTgy++A5zfPAjOq2>J>{eL<^O;m4d|nBq^#iB-ViDK z*zhqr`x+dw9x_$D8K7?>#7i|$J+$wOQB`KR{4OoWL_5Ef(QiRU2V`SF2Sx7q8*)o= zInIm3&grCv@L(#f>?YzeAYk@r_ol?OF?&auP2pc(+e3vkz@sC0 zCE08b+kw?vYWufumqmRkVRK-|qSKNY&9RQqy18w~rTTX!|KyA{&U$QPoWxYxZ#F2p z`0V5@ZFXiQ-A0<-@X(a=64`*eEt^@sAkNl>T&YFVElhnIh89*Q$Mj&*j?GR0d#Uer8(bdOU>vj)_8jGN*a@{o*`jP^B5ZQ(h(0 z7KE9lDtOymBnqVLETqI8k1#f^NExkM7#SP$7t-O|DKyRp9KZgJVEVOgYYAZ`MbOKa znbbzCWKS&OvF;@k$xOav{vnj=YWDbVS2puMAq`FvDlK@-Qqxk~858gz)YN~#i@BM% z(r`95BZbUh*kBqOZd7*3f;wW}Ax7?q8&pYf>eVq00qx;%8f_?QRlFR0r zSVMYA5m+gBnYk@DMnW%YD9!tq)WHBw=Qiluygcl(Wtc>XS7>;C3n^ebJ^dPOi?T>KIqk+?=>CQ2zKc= zKE%9~w@&Fg9Lm2w-PO04sR|#J{p5`Pv3Su6#@OX+bPUHS*L4*7i!&8*O}pa=t|$)5 zMiAC!%2id~mK#ESnp0IiCIkGZqKR`J8Ie9!!*oa`KAPh!!7pU`%4A2)f9=nl)TuFD zV2h4C(N#i{xK0Hfa*(JhwlnC+f%RpuENg`jQ!M^J#a&k?Rz>KL>61YVeo0;gbgUuG z&nli&T~|dkLF;BfwSRmiIUy z*fq`hGgPKp!sMW5F~_tU2xs)F7L$*90$VxE7FiWVSFSyrYQ9gg9l7sySe>+`GBvUf z7mF1EaEVp{NCq!Q%+KBbUiUrY6mKvWyl(8_KM(mcpKO-FJB-PNv>7IK`3(ry%*gHU zq_B#9Fsia;MPZ|pq{dPWT`sxpn4Y-NCwNLk48kLh=Skyz-3i1H?2R_htnzl z>F{68c0h8JWL!GQlRGZ z%46lWVe7z~LJ{ZX%sz=tXZL76JH*7!Lu38rm1fPvs&4rQJ8sP|!)0pVL@F2HjjRAn zWCuM<6oQOb1x-`MlV<97u%m)a;tW8>Kv0=;!jfzC{p+2Fo$|;e0EzWG`FG;7{{B2o zYxv-^h_U>gm_PzN%lWdLAo8Lf0r)K>w>njiD*CgLmn_dJTTog4waMMHtrNEhW;i88 z95uM_$dM2pU-+&V3j{9gY{^YJ%m#gXddpvHYU(JF+WWAmo#L=tg3EWKt+bJyE>~-9 z&!!D6uR%QlT0Bh29#6X|^DdBe)fl^_2hnSsQr2KinVI2Zv zgM^@pR!=^dVN1FV3nMdK+vQIh<~r7J`H-Q4S0d=O9RaSRyI$RMIoL&A$`r$j#utp} zQt1l?s-0dA8)FZ}edodJO?=fHt!v=@A^+Z{pBr$P?XU zzRT+HdL^4w*Ok)e?6Sq~?o`>-N^{GcL$KQ+3YOg_bOfrI5z9j<&0#ADRqMPgb+%??d+$`3=fKII_)z{bW=xkiwFHoH7^g`p8_Vf44 zMZ$T+%l*3eEHaJBy#yO64*0^1ELeN|l-kcn<*=~6y6Vh-YHGOu44fG@?jnTFh{|&^ zJ%IH3a$RRL`FM)QY7?V96tg@}Mu5xlyKZp0N6nRQFLY|#b_%fJ*)3r7q^lx(G180| zuK=`pcK>tx#SFEoE|CE0Dq3ixXW7c!VFy_OQeTz=&2qflUc-t-K3;fYH0vqtP;&={ z2oVe(1=*dSCGE%^lMU5v(qjoD?5~f_u`^kSLNT5{H_6Gtf65lceh0Srb1Umvb0Y(z zp?qnPL3b=HJkxoTYhR-Ftas(0Dgku&z5dr*f`iG|&&laNFpdXDoMzGDKd_I#)#twV ziKg<2_%G{D+C=fJZ`2IIrx=XGjASgl#51;fTQPtZN%`E`TFV#?0dSd6HV{6qGdMtB zMGhF^{nBgGVW-Xbe-H_uj{DxT)V5m{9UgD5)Pl42>YYdV+LH+tcbTHEdyQ&1FIlgO`!Qsb)S8Zo_4dn2)ks7{s=&|3VUp`zb>?x72+4 zJB+1C1D;^#-v{t3@SU^2JvrA8-pA+v0^C}$Y9>h8p?2z4=jQ&HHLtl)UB(b=l~wPn zYruQZ=bDge-%Am4MF7&~_- zpVTB5a&*39tpWIOxfz?KwNnjEn@J>NxBy2)v!?BMITK9nfzOgs%eykWeE~n<5~uHKAxe7ROC0JwvzFr@}g%fjDRm4GoaDx^4b%`t~l;-M=s@vZ+{`M zANwRAopM9S_fec36P>i(5<|2vRtn?rJF}e45jKTfh$~|obZKlbG!_`RdIafljnhWr z774NA>$M7db3b^Q&7r*%;TN*cUvOOEmUvDgS zYlMI{`Gac4EQ=Gr6km%n1LE+$H%kkN4t1OwY8tdXCSjiXpQIGA`#PkxSHJaW!_vC5 zKElWQB!QT;wD?}9u02{Nu+>~|kNC+9^IP_-s1FBBT(=9SyQW+5x1R+&mVKdT3g1KC}5H_IK$uP{>-dXz4Bua=$QV%a{eE5 z@L)GeP`xa!2s#q+AP8|awApdIWbK`DtQhkBc36vl5F%WWfNBb32h~+rm5pjE2qxDD z5Pn#Jl{H*~b1JukW+I_lGH!gWrd|`GaTPv!hk24q^cYa;Ncat={lQNHAXJxujQ)u3^$<_d2{G3=qg>ABTpQL>lntYw zYaw905>4_S*8sY#Rs^E$3DR`uYAaW^KsC<~TqG9b_o8P~ZuLMgSAKV4lH$zlwL4r3 zKPLZ#FQ&;)BK}#&N2oHiTS??9?={wlx$wPpoZTY9EZxrs!ZVJp+tIVCu2&g^Tty*K z&tQXm!I`}&)XxdUuiJwgQ_3GGUc#NZJ?9v`qy5jgRHe3ZDSe(G`Hj{+dY@9G4%o;X z$m}-Q3>2dk%_3xJ*;LL7xJ2MH6tsb{$SROF0eT>QJU9Iotx7`ixB+CmlEPDNzk4lD?%~J@l2W{ZwTSTfIL}&^`IG5 zi9fQB)15n9T+W{B{5aR!I6ci!pKV>j+5eTeyLt*om^4KR?;hh!vRFWAI&?SX?aYHQ zBtm>l;?$ucXAmWcw|w1ThIJp3W8glB?~oCcP`f>j&(jabY}n64cZ#$z-z1kzLiFgldZ7=Ayt}6W`lhx1Uu{ z?hhxD1ZOP9H=PiMgfDk!qK$~Tb(W)J<>j7m{5b}^mcCy-KDYPt{*3Tg!U{&2;o_RtKlD5L#+nhte%k@eJ@=02&*bxc`%OH(iHgQ$ zDO=84_6dI;CHZ28h~i~2{ONsD;Cxs zl1EoXC(P(HC5&GDB#j_&s4s#c&ghJB5KHMMV%{F74_e~MIdLTO{}DB*bZiz)#IG2X zX~zR&?6Q501k*M#ZJnCX@G@_MJexH zuC&SCkg+(&?1e`vEWpd&SRKnnDqOCl%ichBy*eJr-YBWlO&?%jH*Errc61NZ@3iM8 z=!*S2irA)KopJhXxjDyA(zEB#Zb!xwvm|ddxi&bXwJRqN!jd~Q33{kCGXpksa%eGz zFfm}*B!rA4HtY<0nj`BKU_^fg!m&jIMrO<~dwb}UinZt-Ri=8L-!^tv7EjkEi@^>|7`|CUO6FEik=x|rTt;KNgU8V!|Zt;u`%zVhdqS@c(* znz5L=`dw_Lt@SL4as+AWreG*>c(pa8i2r^+LPT;0#?xfi(ig?BE%dh>?g)xw?C85(C|r;p z99b|)01`f`NtnVUzodE8vn*@f9?>+`)EBUzlVN@dCKAZXFsYQe{XrvAU=?MF^QQ_m z!W_E981ncB8p|?+8Ak~sHXqglLiL|fqb4j%%(_lS3st49KmY8+wL$nWQpDkXYWQB{ z4uw^V1i2bc?i=^w#yxgN`q~I^CKb^IXJS7>H#C>-zx276HcQ@I!Yp9rNBAQvFAXfch4U?u(T4dG0Ia+ zz}fSkKvAJ;pL%1jP~ZLmafE)Kk)yB(b>MuFd{sW14f^$MVSCSqesO3HZrir2(6o4t zRJcDx6UhLhD&A9|mYN_yC`9XNN<{)Dq~aF@C0>PK`q4a+DEPMgPKSN>WJhypa+}?! zlY;&cE;QJ12s$5tX+vnI`_PmV!XF&`)dy=_hG2h}bwvb0*2iVOtd4;C&1NPIItkvL zJui?27soQD)m1GE^qz?kN#sm=KO45UpaN$gh#_Ocx4wj?N)h`Eqk-W%j-YE&ogl>jFk zgBKEbkL`3mAHg2`b4vFWU(nHib%OEd*M@e{d@?}0JkyAXOH%%mPWgKuM|M80QsR{a z!5KM=rO6^e6ezwEi#iHD0qawwntjNa(H3pbp{f_T!A;Q=YLqLD^KK{ljfx2OX0SGR z)5FSXkh1Z#W4`#>;i=e_ylzbHJQqntbn2WYd;wCp zzm@OU4kn+{P}Gj=Mc&;__>7d7;wgIeJf`b<)l|Fq9`6?7#F{Er+H1xLeYCf}sxDMrN~JWZfFcOtPbxp?Qvk zk)5Oa*OKdyRzD=$+(6o}BS+dcw$s^4P!7dvo1CL->}#9W0J$3@a^uj|77g6 zSMU+kUlx6>^Jy3P@NI2gY3`Hl8v21FWIZ}AGB#tC#GOTez2yV!dbjP3Q0~9s`Vg-I z>B3R$(*Enh*Qtyab0e06U)+5YowRxacDsCIgH7}n1!&WUH=>Fj8d`C;+cyOFsoO$U zaS?}~MLp=(m*CUgXOWT>HM2SBmsvRI5v8u&JxcW|V6u+FHS>O=1RmZyG#;aI3DeD4 z&T)NsJC@@)M&UhLuIG>Sh-;}kK5T9(VbCT+{6e?OZYsm4?-KdSWqIZP zq4(3YtqIoz^yD$D8~^@@GTI>Vy*~~`p{WCDvTO^=;$y0)wT+s0_oEW#in2?<1}Pd`tq$RMHLgE71-NPh zffYG#4MD%}l1le672i5)FQ(W;`MlYk{b*McosO0U$ir42jwIEMf}=;tnAK9H!Q-IdPgUj?t%cINX&RD*$34v0 zKS*lok0&Y^Uw;f;4EZ+tc8PPpBr=9}iDNQN&f9hI{dKv~+CavdMbYfLYn$6q`YTUf zU%tOi4xi%5_t)~O%NxXY(PnXzWUV0m3u|};uCn2sfj0v5$KWh02_3Lf3)8kVFW#3o z>1HRqSA>yM9NMvpJeJJToN_r?dJ|h2(J@^syfD7R}{FRyB#uGc=25P-?Q>48gy>&zjh$ z1Y0U1C(ur1U%1_mELuB^C}(l@OINWI^~S)T!Gtp*GLYZZa~^stXQRA1EBPd6O~$pK z<&768l(U~xoT93vEQ?gcFjQ(KXXf;@>q^cou3)1QrARqm8xdnvKbvmAT36q~dP7Xk z!dWA)YckT!FpZ1zysNLbvz~vSJYIE8LTatD#X|qY;{Q4IdcRKmWSi}=q21h_16M#_ z9=9ZJoZVdu4k(gA+X$z4`+mfsz43v6OKZ zC{r*7)SGOZ`t5$tyU&f+W7lujZa4NQ*T^NPTEq_W@#0ES)ACFb)nX2@@o7pi)5ON? zJBT~HBu-+R;bjpi;dZF}q(IU|3Rm%U8Sj8+d>GCMbyPvJWU@FZ-z1gbjn)wTGsuneMg6Gh87IRq5aDdpQO|zx1_7YE}}2R03>)yYHw9D5C8_A zWczpBIRFp>5CK2{$N*pf&~FY3_%2BQE=O1X<{<8?w(;@|;L7MBU5oN*;n+Bx*s3#) zLfA6(X_`$c`qITwvn0`#-LP`DeZ#nPJv81Arw!rQ>#0r(*-n12T1OcI9X*iKTKJbW z^k>(wnS7M%SY2d%MyX4|nkzf}JM4|ma?e5BhIp#VI&fzZfwY0iV@@-{89xZJms*R; z0yxj&!9o{(MMNLf6kV2HfsZhF5kKZGGKW4J&{!wuVU>*v1&(5!JVrWN^sa(z&JQZR zZsK^~ZwyCqAO86{q|#kB$}u=$T^bI&*Sa}q5G1qkMAiR%!W!JfH=A>x7*PcFSJu&X z#25l^F1Jn|H_w062#c5f@81;LsUZasW8}~sH^AR0mGcg_Deg#re2DLkG`e{KOepVk znpWdxFz?v`^=Q|ut;zOQ$G`FG|M0t znpnD7B=#>PMP|)4j9G*5s99=$x{1KAH zbYB&pY)b?&MuymaK`>oUkFusV74;)Qvuz~R0wK?!&JiIZM}c++VVogVc>`qH@Bca% zw$}D5R#-TRYQ2!ckA_*Pqf$P_rZkn6^eG!eyFKJ;{FO@A5e+W`MV$T`B_r&v@b&i| zO#F|t3A(>Y|1emHy#(U0ABznUr(${g`$D literal 0 HcmV?d00001 diff --git a/couchpotato/static/images/sprite.png b/couchpotato/static/images/sprite.png index 782229f0cac959a99925d90229ffbd54c26f02eb..0ef3041378b9e5ebca228110d8bb48593c600054 100644 GIT binary patch literal 2759 zcmV;&3OMzNP)gMhaZ72z=`<;X#sq=~3YdqfO*(@h zUtVemThjIa^dd*AK9yZ8UQ|E@%k zBq{5!AeZ%Hcyh^P*k-+GH^O3TTkCLnG+3cW96cp#Xw*-zJ6u;CF0ZG$=8LtE71J9s z!qnXIirwM1TkTH!x!emYA#XT9*;6v&p}FPlR+}SaYOy*FA33%H@)EIDc#n^aFc{5S zP0f}|mG`Q?PIppclks;Cn=PdNVRPWakB+Wp5;5Cd$*O_EW^Ypt8#e5P5s8Vnb+vUH z2|_+eMiB4#e15X({=J>Q-1O=VDCYunAWxU-)3qXq$V-qs{!DJ(n$+p)zbF+772GF- zp^(CCG96pGaQ;rX-v(%B5wU#ri?Pa1WEpPw1%?VSdldC|3PmGd<9W$$X6?7(dQ+xU^xEfbbDP0h#$J7xuOzf z>R5wTZZfwXu-KhstJM(!7!eZk-e*^U`8>fSVYJy`gQ?XL3Wwvu;czFrTrN|{<*uia zM~;MKfUC{;w1(2{@es(vR8hc3>$yPG-eiV{Mq^%le8SJ@7h|QJ4*NfP(*eQs9oyb% z((6yVp^6BIpW43ltzCGJ*SPYunVd2n@qkz=&kJw@e1I@loAHs+go7AO5u|Kom~YSu zkv*YbK%YPKzN&6XIre;b^r&WTub=3tn1spPGRNid6<6tM$IzK2W#tK>P^hrQVtbiu zj>e}P70nh~p55stEp|us&5GN}#U*74R=cAV^IENK-l*O9>ZZxmWKa|tiavDS`~_30YwC;j#ugG$Ti>W%wR%k|rAZWA zWL#~ATEVMU&+tG*<@ft#h%gzBNli@+P^u^-TAy9Iay8-Ul!=;9n4IHwd8*nw0CL;^rOz*$HC;V3wYcWc(dP<_{`J|5D_%;a)<+4?&Ym;n+KsX+f6qC;hOYy-#8gnTY!x-j_E5KD zDOKK1&AfxWvuy?$LG`Gd>WmPhmlf2?B*e8s>9gAI^|vW$+5u?;`Z~rq=Vx$pxF{zF zLD-|ZZ`^M8PBKdHcs%KxYM*0rJnzJb6Ic0&PMtb+4a&?HONL=VPEJlCo9OJ>vxRW) zd%Olozj-R@4 zNw8)=U7bd7dP6CZMo{H!gvf^EY%`zF=U1sz4H)<16uU1|s@T}r-F+DvTw?aPH+h}D zPfTxiF?CFl$R6l%zMTrpwDrpbdBTJV`{Z(YWJrX=VO38J4cl%GVcqxjgp4hwPqLd5 z*;j2)M6L%2J}*^9zbQvaOi9cEC#JM^lE@@e_jt4h9Zcqy5ke0SjgPm-#j40?^Rq}^ z+|Z#`A+lSyZYIISDuyhkQE!w5gWSh76iS7xsOWS07!Mgu1))Pckz-2AoL%B1+DnrO z9ZNxH_+S}pYl9K6%IJCL_qt2UQ6iJb`W&L2Cd7q5;11v%AXborO$PCCfHi!a9Ix+hF2;b2^AF; zw(;Y~XTvjZ@r|>%8#iuTCPjqW+FFA~qfzq?4H)f9ph4S7kwLH5r!&dAeCXS@ZR^O- z&o{%!51l`M-gNx?AtfaxJuNM*Z1m{S zJLBTw-hf7Kqk64<<;s;upv=Z)%a&c6J$p8`*VBR@I!U#_OgOaUZE)fyb(=YTxGpJe|zSH&qEAsw8T$Rx5Sa_#i^#AGSbVeCAia)LEG zefqRYOl_Fux@{HFaWrM@JfU)2%bgvJXWo-_o||Rn?W3+;$^l2<@%hiB<&H!qkx66{ zS?|e6wgav&WCWK-PYfB^Ut;#kASj~{kpkk1B4f4*(H~+G{XH}Y>2s3EBr=I?pe_-W z0tK+v=)8OZ@J&Dy;4$Q}Wls)(F->3bNLBK9N@5V`n9(BM4dPUma-rg~F>eOG<)z!^~ zpqfop2qLe0emarv2wY51(42JwQAL*l39XJ?l{#Wd8o>oRDC zmerVn|0saJ2}2Jl{@1}506lv`^H}ybr8T-lCXo#QyZQLVL=y(sOzj1u&UXUx0pE#Q zMb@4$5}l@vMwqc>2vIaLKj!6NlYvTpjoSb9rl+Up#Kgp8OrJjeU5!RlMi~2%7)bO52#=^|nV0s*!tgIz9HHQqaO=FA78 z+HCvw?R%hd*n@~8fFA(XLQmeJdle=zCq$7!7?+uud4aiWO$VFJW)Grgyff?$Jh`*)#3E&mLv$D>AXqZG3 zYPH&2YLNHtucW9jDJ?DSuP_TrlL`D`Q%}N3-dT{Owk5IwN%n661^~j=!{Cd}TQ2|r N002ovPDHLkV1h8HQ(FK4 delta 1793 zcmV+c2mbiS6@w0t7JmTg2nGNE0LEDivj6}F%1J~)RCwC$n`=yyR~W~Uy9mgTaY?4b z5y1@>yhNvq+tdw^n6Zg|FpG&}b8Jjy!bT?X!z5_t)DLb>lQ9=X2Vvt9k*KJZJ6bNK za%oGU6sUz#T6zOkF2%Fwbf>WcTl!WubvwW0pL5Q0&im(FUVq*m&RZNP{~Gg@D$NJl z;n5qAceLAuClCgzwZp}_5&gKJL-YxRT4eu#mqF@L2{C{G2~OK-Zv2ghmM$RB-J zDvj=29J9yEQ8u9XVMwh*@Z2{F=OK2|p2v`zRHo4P_Q_F?RAv;;-O2^mu_|lNH{9c)dhTCpxW`AV@|rAM$9S0Q*1x{A=*smRRwY}B zRe1UOg@0GVBNp2;Oh?j#Aka%hfn1h$(0%eyK5e(9_PCy{mr{SOjyR`$>9j{T7SFtwNSpWt6tJCN0R3BQ~d{^-evv5kIye z#=Bi!9mJ?tZSA6~E&NV&x8Yv%#!YXpgePnS?6~W3j3*Q|G8v}J*{omQyj{gEkBW(n zAb*==pA{=tg_c#bD=+5!wu8w6IFU*AH%&H?7m4{WnYVf5Vi%H5-+4P5+fXQ!*+2-T zqS;Bc)o=Z!OP5&40=N#habUoPyu7?}Gz}cXbs)yPm7kwqfe1K{V<5ZPR9swKjfL}| z$L1ZQC=|+n12hd1vx-$SNDgC}HLS2EtAA$G^EpM=kTo$do69+SOb(mt=~+#^UOzTx zt2s6{rk|VDBxcXZYEmfwE07&n{dHn0I!roDI!p?M^6cRRuQ!l2Ra8|kvenF^RUks# zHsK?M>oSiX3$pb{+5@y06ly`ZZh@_4PQ=j5f^eO)1BF6)(%boOKwMBh$b;#{FMon| zfc!yTQmM2Z0Xm&-L@X9ZLe?F$6|{ObBfrtn(Gmo}AOBaz$H()K8BkYeFc^NCnwlDi zda+f*va+((GMQ`;nE@9+0MO**AutfhG|lBO`+a zRWYCiIK^cIn1&2(2O(cN!%QR+wW4W6Mn=Y)x4~ru;5js*R;#r$%+8!SbE~PTNex{$ zAc9JzN;GeS%cgmQ9zEdm`MRQ_B9X%*hzEV2m6g?r2uh_=0LgC9LYx}Q1b+^PqY4fV zz5v%8u(SxCRaI3tkPQfhLTPDfX{3WC;BMax-Jd`dtkWZTrO9Wn3$O2U1g#SU6*N1;$Cl&29y zm~+!Py8r=TUsok|} z*B)Fp#tAGItHImbdwQLv7k=B}5Vs0?`G6Q!q^GBcLOd!nGcyF25nuqwxAC{m_Ro%+ zUlW*e(f)e-d-+vJfcG -1){ + self.nav.grab( + new Element('li').grab(el.clone().cloneEvents(el)) + ); + } + }); + + self.added = true; + } + + html.toggleClass('menu_shown'); + + }, + activate: function(name){ var self = this; diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index d2e8fa00..5a7dbfba 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -61,7 +61,7 @@ var CouchPotato = new Class({ new Element('div').adopt( self.block.navigation = new Block.Navigation(self, {}), self.block.search = new Block.Search(self, {}), - self.block.more = new Block.Menu(self, {}) + self.block.more = new Block.Menu(self, {'button_class': 'icon2.cog'}) ) ), self.content = new Element('div.content'), diff --git a/couchpotato/static/scripts/library/question.js b/couchpotato/static/scripts/library/question.js index cab63465..ed53b391 100644 --- a/couchpotato/static/scripts/library/question.js +++ b/couchpotato/static/scripts/library/question.js @@ -7,36 +7,24 @@ var Question = new Class( { self.hint = hint self.answers = answers - self.createQuestion() + self.createQuestion(); self.answers.each(function(answer) { self.createAnswer(answer) }) - self.createMask() }, - createMask : function() { - var self = this - - self.mask = new Element('div.mask').fade('hide').inject(document.body).fade('in'); - }, - createQuestion : function() { + var self = this; - this.container = new Element('div', { - 'class' : 'question' - }).adopt( + self.container = new Element('div.mask.question').adopt( new Element('h3', { 'html': this.question }), new Element('div.hint', { 'html': this.hint }) - ).inject(document.body) - - this.container.position( { - 'position' : 'center' - }); + ).fade('hide').inject(document.body).fade('in') }, @@ -59,17 +47,15 @@ var Question = new Class( { (options.onComplete || function(){})() self.close(); } - })).send(); + })).send(); }); } }, close : function() { var self = this; - self.mask.fade('out'); - (function(){self.mask.destroy()}).delay(1000); - - this.container.destroy(); + self.container.fade('out'); + (function(){self.container.destroy()}).delay(1000); }, toElement : function() { diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 91bd1524..3d016a8d 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -1,19 +1,14 @@ -html { +body, html { color: #fff; font-size: 12px; line-height: 1.5; - font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; + font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; height: 100%; - text-shadow: 0 1px 0 #000; -} - -body { margin: 0; padding: 0; background: #4e5969; - overflow-y: scroll; - height: 100%; } + body { overflow-y: scroll; } body.noscroll { overflow: hidden; } #clean { @@ -32,14 +27,16 @@ pre { } input, textarea { - font-size: 12px; + font-size: 1em; font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; } +input:focus, textarea:focus { + outline: none; +} input:-moz-placeholder, textarea:-moz-placeholder { color: rgba(255, 255, 255, 0.6); } - ::-webkit-input-placeholder, ::-webkit-textarea-placeholder { color: rgba(255, 255, 255, 0.6); } @@ -59,53 +56,30 @@ a:hover { color: #f3f3f3; } .page { display: none; - width: 960px; + width: 100%; + max-width: 980px; margin: 0 auto; - line-height: 24px; - padding: 0 0 20px; + line-height: 1.5em; + padding: 0 15px 20px; } .page.active { display: block; } - .page .noticeMe { - background-color: lightgoldenrodyellow; - display: block; - padding: 20px 10px; - margin: 0 -10px 40px; - font-size: 19px; - text-align: center; - } - .content { clear:both; - padding: 80px 0 10px; + padding: 65px 0 10px; + background: #4e5969; } -h2 { - font-size: 30px; - padding: 0; - margin: 20px 0 0 0; -} - -.footer { - text-align:center; - padding: 50px 0 0 0; - color: #999; - font-size: 10px; - clear: both; -} - - .footer .check { - color: #333; + @media all and (max-width: 480px) { + .content { + padding-top: 40px; + } } -#toTop { - background: black; - position: fixed; - bottom: 0; - right: 0; - padding: 10px 10px 10px 40px; - background: #f7f7f7 url('../images/toTop.gif') no-repeat 10px center; - border-radius: 5px 0 0 0; +h2 { + font-size: 2.5em; + padding: 0; + margin: 20px 0 0 0; } form { @@ -126,6 +100,12 @@ body > .spinner, .mask{ width: 100%; padding: 200px; } + + @media all and (max-width: 480px) { + body > .mask { + padding: 20px; + } + } .button { background: #5082bc url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAyCAYAAACd+7GKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAClJREFUeNpi/v//vwMTAwPDfzjBgMpFI/7hFSOT9Y8qRuF3JLoHAQIMAHYtMmRA+CugAAAAAElFTkSuQmCC") repeat-x; @@ -135,8 +115,6 @@ body > .spinner, .mask{ font-weight: bold; line-height: 1; border-radius: 2px; - box-shadow: 0 1px 2px rgba(0,0,0,0.3); - text-shadow: 0 -1px 1px rgba(0,0,0,0.25); cursor: pointer; } .button.red { background-color: #ff0000; } @@ -164,132 +142,300 @@ body > .spinner, .mask{ .icon.spinner { background-image: url('../images/icon.spinner.gif'); } .icon.attention { background-image: url('../images/icon.attention.png'); } +.icon2 { + display: inline-block; + background: center no-repeat; + font-family: 'Elusive-Icons'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + font-size: 15px; +} + +.icon2.cog:before { content: "\e109"; } +.icon2.eye-open:before { content: "\e09d"; } +.icon2.search:before { content: "\e03e"; } +.icon2.return-key:before { content: "\e111"; } +.icon2.menu:before { + content: "\e076 \e076 \e076"; + line-height: 6px; + transform: scaleX(2); + width: 20px; + font-size: 10px; + display: inline-block; + vertical-align: middle; +} + /*** Navigation ***/ .header { - background: #4e5969; - padding: 10px 0; - height: 80px; + height: 66px; position: fixed; margin: 0; width: 100%; z-index: 5; - box-shadow: 0 20px 30px -30px rgba(0,0,0,0.05); - transition: box-shadow .4s cubic-bezier(0.9,0,0.1,1); + background: #4e5969; + box-shadow: 0 0 10px rgba(0,0,0,.1); + transition: all .4s ease-in-out; } .header.with_shadow { - box-shadow: 0 20px 30px -30px rgba(0,0,0,0.3); + background-color: #46505e; + } + + @media all and (max-width: 480px) { + .header { + height: 44px; + } } .header > div { - width: 960px; + width: 100%; + max-width: 980px; margin: 0 auto; - overflow: hidden; + position: relative; + height: 100%; + padding: 0 15px; } + .header .navigation { display: inline-block; vertical-align: middle; - width: 67.2%; + position: absolute; + height: 100%; + left: 0; + bottom: 0; } - .header .navigation ul { - margin: 0; - padding: 0; - } - .header .navigation li { - color: #fff; - display: inline-block; - font-size:20px; - font-weight: bold; - margin: 0; - text-align: center; - } - - .header .navigation li a { - display: block; - padding: 15px; - position: relative; - } - .header .navigation li:first-child a { padding-left: 10px; } - .header .navigation li span { - display: block; - margin-top: 5px; - } - - .header .navigation li a:after { - content: ''; - display: inline-block; - height: 2px; - width: 76%; - left: 12%; - position: absolute; - top: 46px; - background-color: #46505e; - outline: none; - box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15); - transition: all .4s cubic-bezier(0.9,0,0.1,1); - } - - .header .navigation li:hover a:after { background-color: #047792; } - .header .navigation li.active a:after { background-color: #04bce6; } - - .header .navigation li.disabled { color: #e5e5e5; } - .header .navigation li a { color: #fff; } - - .header .navigation .backtotop { - opacity: 0; - display: block; - width: 80px; - left: 50%; - position: absolute; + + .header .foldout { + width: 44px; + height: 100%; text-align: center; - margin: -10px 0 0 -40px; - background: #4e5969; - padding: 5px 0; - border-radius: 0 0 5px 5px; - color: rgba(255,255,255,.4); - text-shadow: none; - font-weight: normal; + border-right: 1px solid rgba(255,255,255,.07); + display: none; + vertical-align: top; + line-height: 42px; + color: #FFF; + } + + .header .logo { + display: inline-block; + font-size: 1.75em; + padding: 13px 30px 0 15px; + height: 100%; + vertical-align: middle; + border-right: 1px solid rgba(255,255,255,.07); + color: #FFF; + font-weight: normal; + } + + @media all and (max-width: 480px) { + .header .foldout { + display: inline-block; + } + + .header .logo { + padding-top: 7px; + border: 0; + } + } + + @media all and (min-width: 481px) and (max-width: 640px) { + + .header .logo { + display: none; + } + + } + + .header .navigation ul { + display: inline-block; + margin: 0; + padding: 0; + height: 100%; + } + + .header .navigation li { + color: #fff; + display: inline-block; + font-size: 1.75em; + margin: 0; + text-align: center; + height: 100%; + border: 1px solid rgba(255,255,255,.07); + border-width: 0 0 0 1px; + } + .header .navigation li:first-child { + border: none; + } + + .header .navigation li a { + display: block; + padding: 15px; + position: relative; + height: 100%; + border: 1px solid transparent; + border-width: 0 0 4px 0; + font-weight: normal; + } + + .header .navigation li:hover a { border-color: #047792; } + .header .navigation li.active a { border-color: #04bce6; } + + .header .navigation li.disabled { color: #e5e5e5; } + .header .navigation li a { color: #fff; } + + .header .navigation .backtotop { + opacity: 0; + display: block; + width: 80px; + left: 50%; + position: fixed; + bottom: 0; + text-align: center; + margin: -10px 0 0 -40px; + background: #4e5969; + padding: 5px 0; + color: rgba(255,255,255,.4); + font-weight: normal; + } + .header:hover .navigation .backtotop { color: #fff; } + + @media all and (max-width: 480px) { + + body { + position: absolute; + width: 100%; + transition: all .5s cubic-bezier(0.9,0,0.1,1); + left: 0; + } + + .menu_shown body { + left: 160px; + } + + .header .navigation { + height: 100%; + } + + .menu_shown .header .navigation .overlay { + position: fixed; + right: 0; + top: 0; + bottom: 0; + left: 160px; + } + + .header .navigation ul { + width: 160px; + position: fixed; + left: -160px; + background: rgba(0,0,0,.5); + transition: all .5s cubic-bezier(0.9,0,0.1,1); + } + + .menu_shown .header .navigation ul { + left: 0; + } + + .header .navigation ul li { + display: block; + text-align: left; + border-width: 1px 0 0 0; + height: 44px; + } + .header .navigation ul li a { + border-width: 0 4px 0 0; + padding: 5px 20px; + } + + .header .navigation ul li.separator { + background-color: rgba(255,255,255, .07); + height: 5px; + } } - .header:hover .navigation .backtotop { color: #fff; } .header .more_menu { - margin-left: 12px; + position: absolute; + right: 15px; + height: 100%; + border-left: 1px solid rgba(255,255,255,.07); } + + @media all and (max-width: 480px) { + .header .more_menu { + display: none; + } + } + + .header .more_menu .button { + height: 100%; + width: 44px; + border: 0; + box-shadow: none; + border-radius: 0; + background: none; + line-height: 66px; + text-align: center; + padding: 0; + border: 1px solid transparent; + border-width: 0 0 4px; + } + .header .more_menu .button:hover { + background: none; + border-color: #047792; + } + .header .more_menu .wrapper { width: 150px; - margin-left: -110px; - } - .header .more_menu .wrapper:before { - margin-left: -34px; + margin-left: -106px; + margin-top: 66px; } + + @media all and (max-width: 480px) { + .header .more_menu .button { + line-height: 44px; + } + + .header .more_menu .wrapper { + margin-top: 44px; + } + } .header .more_menu .red { color: red; } .header .more_menu .orange { color: orange; } .badge { position: absolute; - width: 14px; - height: 14px; + width: 20px; + height: 20px; text-align: center; - line-height: 14px; - border-radius: 50%; - font-size: 8px; - margin: -5px 0 0 15px; - box-shadow: inset 0 1px 0 rgba(255,255,255,.6), 0 0 3px rgba(0,0,0,.7); + line-height: 20px; + margin: 0; background-color: #1b79b8; - text-shadow: none; - background-image: -*-linear-gradient(0deg, rgba(255,255,255,.3) 0%, rgba(255,255,255,.1) 100%); + top: 0; + right: 0; } + + .header .notification_menu { + right: 60px; + display: block; + } + + @media all and (max-width: 480px) { + .header .notification_menu { + right: 0; + } + } .header .notification_menu .wrapper { width: 300px; - margin-left: -260px; + margin-left: -255px; text-align: left; } - .header .notification_menu .wrapper:before { - left: 296px; - } - .header .notification_menu ul { max-height: 300px; overflow: auto; @@ -309,7 +455,7 @@ body > .spinner, .mask{ .header .notification_menu li:last-child > span { border: 0; } .header .notification_menu li .added { display: block; - font-size: 10px; + font-size: .85em; color: #aaa; text-align: ; } @@ -454,13 +600,9 @@ body > .spinner, .mask{ display: block; width: 600px; padding: 20px; - background: #f5f5f5; position:fixed; - z-index:101; + z-index: 101; text-align: center; - background: #5c697b; - border-radius: 3px; - box-shadow: 0 0 50px rgba(0,0,0,0.55); } .question h3 { @@ -472,7 +614,6 @@ body > .spinner, .mask{ .question .hint { font-size: 14px; color: #ccc; - text-shadow: none; } .question .answer { @@ -495,88 +636,74 @@ body > .spinner, .mask{ background-color: #4c5766; } - .more_menu { - display: inline-block; - vertical-align: middle; - } +.more_menu { + display: inline-block; + vertical-align: middle; +} - .more_menu > a { +.more_menu > a { + display: block; + background: url('../images/sprite.png') no-repeat center -137px; + height: 25px; + width: 25px; + border: 1px solid rgba(0,0,0,0.3); + transition: all 0.3s ease-in-out; +} +.more_menu.show > a:not(:active), .more_menu > a:hover:not(:active) { + background-color: #406db8; +} + +.more_menu .wrapper { + display: none; + background: rgba(255,255,255,0.98); + padding: 4px; + position: absolute; + z-index: 90; + margin: 32px 0 0 -145px; + width: 185px; + box-shadow: 0 20px 20px -5px rgba(0,0,0,0.1); + text-align: center; + color: #000; + background-image: -*-linear-gradient( + 45deg, + rgb(200,200,200) 0%, + rgb(255,255,255) 100% + ); +} + + .more_menu.show .wrapper { display: block; - background: url('../images/sprite.png') no-repeat center -137px; - height: 25px; - width: 25px; - border: 1px solid rgba(0,0,0,0.3); - transition: all 0.3s ease-in-out; - } - .more_menu.show > a:not(:active), .more_menu > a:hover:not(:active) { - background-color: #406db8; } - .more_menu .wrapper { - display: none; - border: 1px solid #333; - background: rgba(255,255,255,0.98); - border-radius: 3px; - padding: 4px !important; - position: absolute; - z-index: 9; - margin: 32px 0 0 -145px; - width: 185px; - box-shadow: 0 10px 10px -5px rgba(0,0,0,0.4); - text-align: center; - color: #000; - text-shadow: none; - background-image: -*-linear-gradient( - 45deg, - rgb(200,200,200) 0%, - rgb(255,255,255) 100% - ); + .more_menu ul { + padding: 0; + margin: 0; + list-style: none; } - .more_menu .wrapper:before { - content: ' '; - height: 0; - position: relative; - width: 0; - border: 6px solid transparent; - border-bottom-color: #fff; - display: block; - top: -16px; - left: 146px; - } - .more_menu.show .wrapper { + .more_menu .wrapper li { + width: 100%; + height: auto; + } + + .more_menu .wrapper li a { display: block; + border-bottom: 1px solid rgba(255,255,255,0.2); + box-shadow: none; + font-weight: normal; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + padding: 3px 0; + color: #000; } - .more_menu ul { - padding: 0; - margin: -12px 0 0 0; - list-style: none; + .more_menu .wrapper li:last-child a { + border: none; } - - .more_menu .wrapper li { - width: 100%; - height: auto; + .more_menu .wrapper li a:hover { + background: rgba(0,0,0,0.05); } - - .more_menu .wrapper li a { - display: block; - border-bottom: 1px solid rgba(255,255,255,0.2); - box-shadow: none; - font-weight: normal; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 1px; - padding: 3px 0; - color: #000; - } - - .more_menu .wrapper li:last-child a { - border: none; - } - .more_menu .wrapper li a:hover { - background: rgba(0,0,0,0.05); - } .messages { position: fixed; @@ -619,4 +746,64 @@ body > .spinner, .mask{ .messages .message.hide { margin-left: 240px; opacity: 0; - } \ No newline at end of file + } + +/* Fonts */ +@font-face { + font-family: 'Elusive-Icons'; + src:url('../fonts/Elusive-Icons.eot'); + src:url('../fonts/Elusive-Icons.eot?#iefix') format('embedded-opentype'), + url('../fonts/Elusive-Icons.woff') format('woff'), + url('../fonts/Elusive-Icons.ttf') format('truetype'), + url('../fonts/Elusive-Icons.svg#Elusive-Icons') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'OpenSans'; + src: url('../fonts/OpenSans-Regular-webfont.eot'); + src: url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), + url('../fonts/OpenSans-Regular-webfont.ttf') format('truetype'), + url('../fonts/OpenSans-Regular-webfont.svg#OpenSansRegular') format('svg'); + font-weight: normal; + font-style: normal; + +} + +@font-face { + font-family: 'OpenSans'; + src: url('../fonts/OpenSans-Italic-webfont.eot'); + src: url('../fonts/OpenSans-Italic-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Italic-webfont.woff') format('woff'), + url('../fonts/OpenSans-Italic-webfont.ttf') format('truetype'), + url('../fonts/OpenSans-Italic-webfont.svg#OpenSansItalic') format('svg'); + font-weight: normal; + font-style: italic; + +} + +@font-face { + font-family: 'OpenSans'; + src: url('../fonts/OpenSans-Bold-webfont.eot'); + src: url('../fonts/OpenSans-Bold-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Bold-webfont.woff') format('woff'), + url('../fonts/OpenSans-Bold-webfont.ttf') format('truetype'), + url('../fonts/OpenSans-Bold-webfont.svg#OpenSansBold') format('svg'); + font-weight: bold; + font-style: normal; + +} + +@font-face { + font-family: 'OpenSans'; + src: url('../fonts/OpenSans-BoldItalic-webfont.eot'); + src: url('../fonts/OpenSans-BoldItalic-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-BoldItalic-webfont.woff') format('woff'), + url('../fonts/OpenSans-BoldItalic-webfont.ttf') format('truetype'), + url('../fonts/OpenSans-BoldItalic-webfont.svg#OpenSansBoldItalic') format('svg'); + font-weight: bold; + font-style: italic; + +} \ No newline at end of file diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index a5655f88..638a6585 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -1,5 +1,9 @@ +.page.settings { + min-width: 960px; +} + .page.settings:after { - content: "."; + content: ""; display: block; clear: both; visibility: hidden; @@ -9,15 +13,15 @@ .page.settings .tabs { float: left; - width: 20%; - font-size: 20px; + width: 14.7%; + font-size: 17px; text-align: right; list-style: none; - padding: 40px 0; + padding: 35px 0; margin: 0; min-height: 470px; background-image: -*-linear-gradient( - 20deg, + 14deg, rgba(0,0,0,0) 50%, rgba(0,0,0,0.3) 100% ); @@ -60,9 +64,9 @@ .page.settings .containers { - width: 80%; + width: 84%; float: left; - padding: 20px 2%; + padding: 40px 2%; min-height: 300px; } @@ -240,7 +244,7 @@ display: block; text-align: right; height: 20px; - margin: 0; + margin: 0 0 -37px; } .page .advanced_toggle span { padding: 0 5px; } .page.show_advanced .advanced_toggle { diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html index 1d618066..4fcb5c9e 100644 --- a/couchpotato/templates/index.html +++ b/couchpotato/templates/index.html @@ -1,6 +1,9 @@ + + + {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %} {% endfor %} {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %} From 924bed06cbfc20d170e86d75d58652c684e8ec83 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 30 Apr 2013 00:29:25 +0200 Subject: [PATCH 090/127] Rewrite font css --- couchpotato/core/_base/clientscript/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py index f2a30f6f..4938a22a 100644 --- a/couchpotato/core/_base/clientscript/main.py +++ b/couchpotato/core/_base/clientscript/main.py @@ -110,6 +110,7 @@ class ClientScript(Plugin): else: data = cssmin(f) data = data.replace('../images/', '../static/images/') + data = data.replace('../fonts/', '../static/fonts/') raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data}) From 7b3a1409d59be095299520c0f275541bccba6b3a Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 30 Apr 2013 10:32:22 +0200 Subject: [PATCH 091/127] Force thumbnail view on home --- couchpotato/core/plugins/movie/static/list.js | 5 +++-- couchpotato/static/scripts/page/home.js | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index 46819439..e1cb0d80 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -8,7 +8,8 @@ var MovieList = new Class({ load_more: true, loader: true, menu: [], - add_new: false + add_new: false, + force_view: false }, movies: [], @@ -43,7 +44,7 @@ var MovieList = new Class({ }) : null ); - if($(window).getSize().x < 480) + if($(window).getSize().x < 480 && !self.options.force_view) self.changeView('list'); else self.changeView(self.getSavedView() || self.options.view || 'details'); diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index 1839e3b0..e1fe63e0 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -59,6 +59,7 @@ Page.Home = new Class({ 'actions': [MA.IMDB, MA.Refresh], 'load_more': false, 'view': 'thumbs', + 'force_view': true, 'api_call': 'dashboard.soon' }); From 12c3fc6ce31a4d2fabde19b249eb56fc78b741cd Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 30 Apr 2013 11:13:47 +0200 Subject: [PATCH 092/127] Don't reverse result order --- couchpotato/core/event.py | 1 - 1 file changed, 1 deletion(-) diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index 8a5145a6..b46b2b4f 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -102,7 +102,6 @@ def fireEvent(name, *args, **kwargs): # Merge if options['merge'] and len(results) > 0: - results.reverse() # Priority 1 is higher then 100 # Dict if isinstance(results[0], dict): From 30ec8216e15262d6332a7fc0f3faa9b23595776f Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 30 Apr 2013 13:17:03 +0200 Subject: [PATCH 093/127] Minify on backend --- couchpotato/core/_base/clientscript/main.py | 12 +- .../core/plugins/profile/static/profile.css | 12 +- .../static/scripts/library/prefix_free.js | 487 ------- couchpotato/static/style/main.css | 16 +- couchpotato/static/style/settings.css | 16 +- libs/cssprefixer/__init__.py | 20 + libs/cssprefixer/engine.py | 117 ++ libs/cssprefixer/rules.py | 271 ++++ libs/cssutils/__init__.py | 385 +++++ libs/cssutils/_codec2.py | 584 ++++++++ libs/cssutils/_codec3.py | 608 ++++++++ libs/cssutils/_fetch.py | 44 + libs/cssutils/_fetchgae.py | 68 + libs/cssutils/codec.py | 16 + libs/cssutils/css/__init__.py | 80 ++ libs/cssutils/css/colors.py | 184 +++ libs/cssutils/css/csscharsetrule.py | 159 +++ libs/cssutils/css/csscomment.py | 87 ++ libs/cssutils/css/cssfontfacerule.py | 184 +++ libs/cssutils/css/cssimportrule.py | 396 ++++++ libs/cssutils/css/cssmediarule.py | 302 ++++ libs/cssutils/css/cssnamespacerule.py | 295 ++++ libs/cssutils/css/csspagerule.py | 436 ++++++ libs/cssutils/css/cssproperties.py | 122 ++ libs/cssutils/css/cssrule.py | 304 ++++ libs/cssutils/css/cssrulelist.py | 53 + libs/cssutils/css/cssstyledeclaration.py | 697 +++++++++ libs/cssutils/css/cssstylerule.py | 234 +++ libs/cssutils/css/cssstylesheet.py | 804 +++++++++++ libs/cssutils/css/cssunknownrule.py | 209 +++ libs/cssutils/css/cssvalue.py | 1251 +++++++++++++++++ libs/cssutils/css/cssvariablesdeclaration.py | 330 +++++ libs/cssutils/css/cssvariablesrule.py | 198 +++ libs/cssutils/css/marginrule.py | 215 +++ libs/cssutils/css/property.py | 510 +++++++ libs/cssutils/css/selector.py | 813 +++++++++++ libs/cssutils/css/selectorlist.py | 234 +++ libs/cssutils/css/value.py | 871 ++++++++++++ libs/cssutils/css2productions.py | 131 ++ libs/cssutils/cssproductions.py | 124 ++ libs/cssutils/errorhandler.py | 118 ++ libs/cssutils/helper.py | 137 ++ libs/cssutils/parse.py | 232 +++ libs/cssutils/prodparser.py | 733 ++++++++++ libs/cssutils/profiles.py | 791 +++++++++++ libs/cssutils/sac.py | 428 ++++++ libs/cssutils/script.py | 362 +++++ libs/cssutils/scripts/__init__.py | 4 + libs/cssutils/scripts/csscapture.py | 69 + libs/cssutils/scripts/csscombine.py | 94 ++ libs/cssutils/scripts/cssparse.py | 62 + libs/cssutils/serialize.py | 1138 +++++++++++++++ libs/cssutils/settings.py | 15 + libs/cssutils/stylesheets/__init__.py | 11 + libs/cssutils/stylesheets/medialist.py | 235 ++++ libs/cssutils/stylesheets/mediaquery.py | 207 +++ libs/cssutils/stylesheets/stylesheet.py | 123 ++ libs/cssutils/stylesheets/stylesheetlist.py | 32 + libs/cssutils/tokenize2.py | 223 +++ libs/cssutils/util.py | 884 ++++++++++++ libs/encutils/__init__.py | 690 +++++++++ libs/minify/cssmin.py | 223 --- 62 files changed, 17953 insertions(+), 737 deletions(-) delete mode 100644 couchpotato/static/scripts/library/prefix_free.js create mode 100755 libs/cssprefixer/__init__.py create mode 100755 libs/cssprefixer/engine.py create mode 100755 libs/cssprefixer/rules.py create mode 100755 libs/cssutils/__init__.py create mode 100755 libs/cssutils/_codec2.py create mode 100755 libs/cssutils/_codec3.py create mode 100755 libs/cssutils/_fetch.py create mode 100755 libs/cssutils/_fetchgae.py create mode 100755 libs/cssutils/codec.py create mode 100755 libs/cssutils/css/__init__.py create mode 100755 libs/cssutils/css/colors.py create mode 100755 libs/cssutils/css/csscharsetrule.py create mode 100755 libs/cssutils/css/csscomment.py create mode 100755 libs/cssutils/css/cssfontfacerule.py create mode 100755 libs/cssutils/css/cssimportrule.py create mode 100755 libs/cssutils/css/cssmediarule.py create mode 100755 libs/cssutils/css/cssnamespacerule.py create mode 100755 libs/cssutils/css/csspagerule.py create mode 100755 libs/cssutils/css/cssproperties.py create mode 100755 libs/cssutils/css/cssrule.py create mode 100755 libs/cssutils/css/cssrulelist.py create mode 100755 libs/cssutils/css/cssstyledeclaration.py create mode 100755 libs/cssutils/css/cssstylerule.py create mode 100755 libs/cssutils/css/cssstylesheet.py create mode 100755 libs/cssutils/css/cssunknownrule.py create mode 100755 libs/cssutils/css/cssvalue.py create mode 100755 libs/cssutils/css/cssvariablesdeclaration.py create mode 100755 libs/cssutils/css/cssvariablesrule.py create mode 100755 libs/cssutils/css/marginrule.py create mode 100755 libs/cssutils/css/property.py create mode 100755 libs/cssutils/css/selector.py create mode 100755 libs/cssutils/css/selectorlist.py create mode 100755 libs/cssutils/css/value.py create mode 100755 libs/cssutils/css2productions.py create mode 100755 libs/cssutils/cssproductions.py create mode 100755 libs/cssutils/errorhandler.py create mode 100755 libs/cssutils/helper.py create mode 100755 libs/cssutils/parse.py create mode 100755 libs/cssutils/prodparser.py create mode 100755 libs/cssutils/profiles.py create mode 100755 libs/cssutils/sac.py create mode 100755 libs/cssutils/script.py create mode 100755 libs/cssutils/scripts/__init__.py create mode 100755 libs/cssutils/scripts/csscapture.py create mode 100755 libs/cssutils/scripts/csscombine.py create mode 100755 libs/cssutils/scripts/cssparse.py create mode 100755 libs/cssutils/serialize.py create mode 100755 libs/cssutils/settings.py create mode 100755 libs/cssutils/stylesheets/__init__.py create mode 100755 libs/cssutils/stylesheets/medialist.py create mode 100755 libs/cssutils/stylesheets/mediaquery.py create mode 100755 libs/cssutils/stylesheets/stylesheet.py create mode 100755 libs/cssutils/stylesheets/stylesheetlist.py create mode 100755 libs/cssutils/tokenize2.py create mode 100755 libs/cssutils/util.py create mode 100755 libs/encutils/__init__.py delete mode 100644 libs/minify/cssmin.py diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py index 4938a22a..9536463e 100644 --- a/couchpotato/core/_base/clientscript/main.py +++ b/couchpotato/core/_base/clientscript/main.py @@ -1,10 +1,11 @@ from couchpotato.core.event import addEvent +from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -from minify.cssmin import cssmin from minify.jsmin import jsmin +import cssprefixer import os import traceback @@ -23,7 +24,6 @@ class ClientScript(Plugin): 'script': [ 'scripts/library/mootools.js', 'scripts/library/mootools_more.js', - 'scripts/library/prefix_free.js', 'scripts/library/uniform.js', 'scripts/library/form_replacement/form_check.js', 'scripts/library/form_replacement/form_radio.js', @@ -69,7 +69,8 @@ class ClientScript(Plugin): addEvent('clientscript.get_styles', self.getStyles) addEvent('clientscript.get_scripts', self.getScripts) - addEvent('app.load', self.minify) + if not Env.get('dev'): + addEvent('app.load', self.minify) self.addCore() @@ -108,9 +109,10 @@ class ClientScript(Plugin): if file_type == 'script': data = jsmin(f) else: - data = cssmin(f) + data = cssprefixer.process(f, debug = False, minify = True) data = data.replace('../images/', '../static/images/') data = data.replace('../fonts/', '../static/fonts/') + data = data.replace('../../static/', '../static/') # Replace inside plugins raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data}) @@ -120,7 +122,7 @@ class ClientScript(Plugin): data += self.comment.get(file_type) % (r.get('file'), r.get('date')) data += r.get('data') + '\n\n' - self.createFile(out, data.strip()) + self.createFile(out, ss(data.strip())) if not self.minified.get(file_type): self.minified[file_type] = {} diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css index 9d50d2fd..26ee64c7 100644 --- a/couchpotato/core/plugins/profile/static/profile.css +++ b/couchpotato/core/plugins/profile/static/profile.css @@ -71,13 +71,13 @@ } .profile .types .type .handle { - background: url('./handle.png') center; + background: url('../../static/profile_plugin/handle.png') center; display: inline-block; height: 20px; width: 20px; - cursor: grab; - cursor: -moz-grab; cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; margin: 0; } @@ -105,9 +105,9 @@ } #profile_ordering li { - cursor: grab; - cursor: -moz-grab; cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; border-bottom: 1px solid rgba(255,255,255,0.2); padding: 0 5px; } @@ -126,7 +126,7 @@ } #profile_ordering li .handle { - background: url('./handle.png') center; + background: url('../../static/profile_plugin/handle.png') center; width: 20px; float: right; } diff --git a/couchpotato/static/scripts/library/prefix_free.js b/couchpotato/static/scripts/library/prefix_free.js deleted file mode 100644 index b6d9812a..00000000 --- a/couchpotato/static/scripts/library/prefix_free.js +++ /dev/null @@ -1,487 +0,0 @@ -/** - * StyleFix 1.0.3 & PrefixFree 1.0.7 - * @author Lea Verou - * MIT license - */ - -(function(){ - -if(!window.addEventListener) { - return; -} - -var self = window.StyleFix = { - link: function(link) { - try { - // Ignore stylesheets with data-noprefix attribute as well as alternate stylesheets - if(link.rel !== 'stylesheet' || link.hasAttribute('data-noprefix')) { - return; - } - } - catch(e) { - return; - } - - var url = link.href || link.getAttribute('data-href'), - base = url.replace(/[^\/]+$/, ''), - base_scheme = (/^[a-z]{3,10}:/.exec(base) || [''])[0], - base_domain = (/^[a-z]{3,10}:\/\/[^\/]+/.exec(base) || [''])[0], - base_query = /^([^?]*)\??/.exec(url)[1], - parent = link.parentNode, - xhr = new XMLHttpRequest(), - process; - - xhr.onreadystatechange = function() { - if(xhr.readyState === 4) { - process(); - } - }; - - process = function() { - var css = xhr.responseText; - - if(css && link.parentNode && (!xhr.status || xhr.status < 400 || xhr.status > 600)) { - css = self.fix(css, true, link); - - // Convert relative URLs to absolute, if needed - if(base) { - css = css.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi, function($0, quote, url) { - if(/^([a-z]{3,10}:|#)/i.test(url)) { // Absolute & or hash-relative - return $0; - } - else if(/^\/\//.test(url)) { // Scheme-relative - // May contain sequences like /../ and /./ but those DO work - return 'url("' + base_scheme + url + '")'; - } - else if(/^\//.test(url)) { // Domain-relative - return 'url("' + base_domain + url + '")'; - } - else if(/^\?/.test(url)) { // Query-relative - return 'url("' + base_query + url + '")'; - } - else { - // Path-relative - return 'url("' + base + url + '")'; - } - }); - - // behavior URLs shoudn’t be converted (Issue #19) - // base should be escaped before added to RegExp (Issue #81) - var escaped_base = base.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1"); - css = css.replace(RegExp('\\b(behavior:\\s*?url\\(\'?"?)' + escaped_base, 'gi'), '$1'); - } - - var style = document.createElement('style'); - style.textContent = css; - style.media = link.media; - style.disabled = link.disabled; - style.setAttribute('data-href', link.getAttribute('href')); - - parent.insertBefore(style, link); - parent.removeChild(link); - - style.media = link.media; // Duplicate is intentional. See issue #31 - } - }; - - try { - xhr.open('GET', url); - xhr.send(null); - } catch (e) { - // Fallback to XDomainRequest if available - if (typeof XDomainRequest != "undefined") { - xhr = new XDomainRequest(); - xhr.onerror = xhr.onprogress = function() {}; - xhr.onload = process; - xhr.open("GET", url); - xhr.send(null); - } - } - - link.setAttribute('data-inprogress', ''); - }, - - styleElement: function(style) { - if (style.hasAttribute('data-noprefix')) { - return; - } - var disabled = style.disabled; - - style.textContent = self.fix(style.textContent, true, style); - - style.disabled = disabled; - }, - - styleAttribute: function(element) { - var css = element.getAttribute('style'); - - css = self.fix(css, false, element); - - element.setAttribute('style', css); - }, - - process: function() { - // Linked stylesheets - $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); - - // Inline stylesheets - $('style').forEach(StyleFix.styleElement); - - // Inline styles - $('[style]').forEach(StyleFix.styleAttribute); - }, - - register: function(fixer, index) { - (self.fixers = self.fixers || []) - .splice(index === undefined? self.fixers.length : index, 0, fixer); - }, - - fix: function(css, raw, element) { - for(var i=0; i -1) { - // Gradients are supported with a prefix, convert angles to legacy - css = css.replace(/(\s|:|,)(repeating-)?linear-gradient\(\s*(-?\d*\.?\d*)deg/ig, function ($0, delim, repeating, deg) { - return delim + (repeating || '') + 'linear-gradient(' + (90-deg) + 'deg'; - }); - } - - css = fix('functions', '(\\s|:|,)', '\\s*\\(', '$1' + prefix + '$2(', css); - css = fix('keywords', '(\\s|:)', '(\\s|;|\\}|$)', '$1' + prefix + '$2$3', css); - css = fix('properties', '(^|\\{|\\s|;)', '\\s*:', '$1' + prefix + '$2:', css); - - // Prefix properties *inside* values (issue #8) - if (self.properties.length) { - var regex = RegExp('\\b(' + self.properties.join('|') + ')(?!:)', 'gi'); - - css = fix('valueProperties', '\\b', ':(.+?);', function($0) { - return $0.replace(regex, prefix + "$1") - }, css); - } - - if(raw) { - css = fix('selectors', '', '\\b', self.prefixSelector, css); - css = fix('atrules', '@', '\\b', '@' + prefix + '$1', css); - } - - // Fix double prefixing - css = css.replace(RegExp('-' + prefix, 'g'), '-'); - - // Prefix wildcard - css = css.replace(/-\*-(?=[a-z]+)/gi, self.prefix); - - return css; - }, - - property: function(property) { - return (self.properties.indexOf(property)? self.prefix : '') + property; - }, - - value: function(value, property) { - value = fix('functions', '(^|\\s|,)', '\\s*\\(', '$1' + self.prefix + '$2(', value); - value = fix('keywords', '(^|\\s)', '(\\s|$)', '$1' + self.prefix + '$2$3', value); - - // TODO properties inside values - - return value; - }, - - // Warning: Prefixes no matter what, even if the selector is supported prefix-less - prefixSelector: function(selector) { - return selector.replace(/^:{1,2}/, function($0) { return $0 + self.prefix }) - }, - - // Warning: Prefixes no matter what, even if the property is supported prefix-less - prefixProperty: function(property, camelCase) { - var prefixed = self.prefix + property; - - return camelCase? StyleFix.camelCase(prefixed) : prefixed; - } -}; - -/************************************** - * Properties - **************************************/ -(function() { - var prefixes = {}, - properties = [], - shorthands = {}, - style = getComputedStyle(document.documentElement, null), - dummy = document.createElement('div').style; - - // Why are we doing this instead of iterating over properties in a .style object? Cause Webkit won't iterate over those. - var iterate = function(property) { - if(property.charAt(0) === '-') { - properties.push(property); - - var parts = property.split('-'), - prefix = parts[1]; - - // Count prefix uses - prefixes[prefix] = ++prefixes[prefix] || 1; - - // This helps determining shorthands - while(parts.length > 3) { - parts.pop(); - - var shorthand = parts.join('-'); - - if(supported(shorthand) && properties.indexOf(shorthand) === -1) { - properties.push(shorthand); - } - } - } - }, - supported = function(property) { - return StyleFix.camelCase(property) in dummy; - } - - // Some browsers have numerical indices for the properties, some don't - if(style.length > 0) { - for(var i=0; i .spinner, .mask{ border-radius:30px; box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20); - background: url('../images/sprite.png') no-repeat 94% -53px, -*-linear-gradient( - 270deg, + background: url('../images/sprite.png') no-repeat 94% -53px, linear-gradient( + 180deg, #5b9bd1 0%, #406db8 100% ); @@ -583,8 +583,8 @@ body > .spinner, .mask{ border: 1px solid #252930; box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2); background: rgb(55,62,74); - background-image: -*-linear-gradient( - 90deg, + background-image: linear-gradient( + 0, rgb(55,62,74) 0%, rgb(73,83,98) 100% ); @@ -664,8 +664,8 @@ body > .spinner, .mask{ box-shadow: 0 20px 20px -5px rgba(0,0,0,0.1); text-align: center; color: #000; - background-image: -*-linear-gradient( - 45deg, + background-image: linear-gradient( + -45deg, rgb(200,200,200) 0%, rgb(255,255,255) 100% ); @@ -725,8 +725,8 @@ body > .spinner, .mask{ overflow: hidden; transition: all .6s cubic-bezier(0.9,0,0.1,1); box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20); - background-image: -*-linear-gradient( - 270deg, + background-image: linear-gradient( + 180deg, #5b9bd1 0%, #406db8 100% ); diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index 638a6585..1f001b7c 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -20,8 +20,8 @@ padding: 35px 0; margin: 0; min-height: 470px; - background-image: -*-linear-gradient( - 14deg, + background-image: linear-gradient( + 76deg, rgba(0,0,0,0) 50%, rgba(0,0,0,0.3) 100% ); @@ -443,16 +443,16 @@ border-radius: 2px; } .page .tag_input > ul:hover > li.choice { - background: -*-linear-gradient( - 270deg, + background: linear-gradient( + 180deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 100% ); } .page .tag_input > ul > li.choice:hover, .page .tag_input > ul > li.choice.selected { - background: -*-linear-gradient( - 270deg, + background: linear-gradient( + 180deg, #5b9bd1 0%, #406db8 100% ); @@ -490,8 +490,8 @@ margin: -9px 0 0 -16px; border-radius: 30px 30px 0 0; cursor: pointer; - background: url('../images/icon.delete.png') no-repeat center 2px, -*-linear-gradient( - 270deg, + background: url('../images/icon.delete.png') no-repeat center 2px, linear-gradient( + 180deg, #5b9bd1 0%, #5b9bd1 100% ); diff --git a/libs/cssprefixer/__init__.py b/libs/cssprefixer/__init__.py new file mode 100755 index 00000000..05674329 --- /dev/null +++ b/libs/cssprefixer/__init__.py @@ -0,0 +1,20 @@ +# CSSPrefixer +# Copyright 2010-2012 Greg V. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import engine +import rules +from engine import process + +__all__ = ('process', 'engine', 'rules') diff --git a/libs/cssprefixer/engine.py b/libs/cssprefixer/engine.py new file mode 100755 index 00000000..894f6e7f --- /dev/null +++ b/libs/cssprefixer/engine.py @@ -0,0 +1,117 @@ +# CSSPrefixer +# Copyright 2010-2012 Greg V. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cssutils +import re +from rules import rules as tr_rules +from rules import prefixRegex + + +keyframesRegex = re.compile(r'@keyframes\s?\w+\s?{(.*)}') +blockRegex = re.compile(r'\w+\s?\{(.*)\}') + + +def magic(ruleset, debug, minify, filt, parser): + if isinstance(ruleset, cssutils.css.CSSUnknownRule): + if ruleset.cssText.startswith('@keyframes'): + inner = parser.parseString(keyframesRegex.split(ruleset.cssText.replace('\n', ''))[1]) + # BUG: doesn't work when minified + s = '' if minify else '\n' + return '@-webkit-keyframes {' + s + \ + ''.join([magic(rs, debug, minify, ['webkit'], parser) for rs in inner]) \ + + '}' + s + '@-moz-keyframes {' + s + \ + ''.join([magic(rs, debug, minify, ['moz'], parser) for rs in inner]) \ + + '}' + s + ruleset.cssText + elif ruleset.cssText.startswith('from') or ruleset.cssText.startswith('to'): + return ''.join([magic(rs, debug, minify, filt, parser) + for rs in parser.parseString(blockRegex.sub(r'\1', ruleset.cssText.replace('\n', ''))[1])]) + else: + return + elif hasattr(ruleset, 'style'): # Comments don't + ruleSet = set() + rules = list() + children = list(ruleset.style.children()) + ruleset.style = cssutils.css.CSSStyleDeclaration() # clear out the styles that were there + for rule in children: + if not hasattr(rule, 'name'): # comments don't have name + rules.append(rule) + continue + name = prefixRegex.sub('', rule.name) + if name in tr_rules: + rule.name = name + if rule.cssText in ruleSet: + continue + ruleSet.add(rule.cssText) + rules.append(rule) + + ruleset.style.seq._readonly = False + for rule in rules: + if not hasattr(rule, 'name'): + ruleset.style.seq.append(rule, 'Comment') + continue + processor = None + try: # try except so if anything goes wrong we don't lose the original property + if rule.name in tr_rules: + processor = tr_rules[rule.name](rule) + [ruleset.style.seq.append(prop, 'Property') for prop in processor.get_prefixed_props(filt) if prop] + # always add the original rule + if processor and hasattr(processor, 'get_base_prop'): + ruleset.style.seq.append(processor.get_base_prop(), 'Property') + else: + ruleset.style.seq.append(rule, 'Property') + except: + if debug: + print 'warning with ' + str(rule) + ruleset.style.seq.append(rule, 'Property') + ruleset.style.seq._readonly = True + elif hasattr(ruleset, 'cssRules'): + for subruleset in ruleset: + magic(subruleset, debug, minify, filt, parser) + cssText = ruleset.cssText + if not cssText: # blank rules return None so return an empty string + return + if minify or not hasattr(ruleset, 'style'): + return unicode(cssText) + return unicode(cssText) + '\n' + + +def process(string, debug = False, minify = False, filt = ['webkit', 'moz', 'o', 'ms'], **prefs): + loglevel = 'DEBUG' if debug else 'ERROR' + parser = cssutils.CSSParser(loglevel = 'CRITICAL') + if minify: + cssutils.ser.prefs.useMinified() + else: + cssutils.ser.prefs.useDefaults() + + # use the passed in prefs + for key, value in prefs.iteritems(): + if hasattr(cssutils.ser.prefs, key): + cssutils.ser.prefs.__dict__[key] = value + + results = [] + sheet = parser.parseString(string) + for ruleset in sheet.cssRules: + cssText = magic(ruleset, debug, minify, filt, parser) + if cssText: + results.append(cssText) + + # format with newlines based on minify + joinStr = '' if minify else '\n' + + # Not using sheet.cssText - it's buggy: + # it skips some prefixed properties. + return joinStr.join(results).rstrip() + +__all__ = ['process'] diff --git a/libs/cssprefixer/rules.py b/libs/cssprefixer/rules.py new file mode 100755 index 00000000..156d4f18 --- /dev/null +++ b/libs/cssprefixer/rules.py @@ -0,0 +1,271 @@ +# CSSPrefixer +# Copyright 2010-2012 Greg V. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import cssutils + +prefixRegex = re.compile('^(-o-|-ms-|-moz-|-webkit-)') + + +class BaseReplacementRule(object): + vendor_prefixes = ['moz', 'webkit'] + + def __init__(self, prop): + self.prop = prop + + def get_prefixed_props(self, filt): + for prefix in [p for p in self.vendor_prefixes if p in filt]: + yield cssutils.css.Property( + name='-%s-%s' % (prefix, self.prop.name), + value=self.prop.value, + priority=self.prop.priority + ) + + @staticmethod + def should_prefix(): + return True + + +class FullReplacementRule(BaseReplacementRule): + vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['o', 'ms']) + + +class BaseAndIEReplacementRule(BaseReplacementRule): + vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['ms']) + + +class BaseAndOperaReplacementRule(BaseReplacementRule): + vendor_prefixes = sorted(BaseReplacementRule.vendor_prefixes + ['o']) + + +class WebkitReplacementRule(BaseReplacementRule): + vendor_prefixes = ['webkit'] + + +class OperaAndIEReplacementRule(BaseReplacementRule): + vendor_prefixes = ['ms', 'o'] + + +class MozReplacementRule(BaseReplacementRule): + vendor_prefixes = ['moz'] + + +class BorderRadiusReplacementRule(BaseReplacementRule): + """ + Mozilla's Gecko engine uses different syntax for rounded corners. + """ + vendor_prefixes = ['webkit'] + + def get_prefixed_props(self, filt): + for prop in BaseReplacementRule.get_prefixed_props(self, filt): + yield prop + if 'moz' in filt: + name = '-moz-' + self.prop.name.replace('top-left-radius', 'radius-topleft') \ + .replace('top-right-radius', 'radius-topright') \ + .replace('bottom-right-radius', 'radius-bottomright') \ + .replace('bottom-left-radius', 'radius-bottomleft') + yield cssutils.css.Property( + name=name, + value=self.prop.value, + priority=self.prop.priority + ) + + +class DisplayReplacementRule(BaseReplacementRule): + """ + Flexible Box Model stuff. + CSSUtils parser doesn't support duplicate properties, so that's dirty. + """ + def get_prefixed_props(self, filt): + if self.prop.value == 'box': # only add prefixes if the value is box + for prefix in [p for p in self.vendor_prefixes if p in filt]: + yield cssutils.css.Property( + name='display', + value='-%s-box' % prefix, + priority=self.prop.priority + ) + + +class TransitionReplacementRule(BaseReplacementRule): + vendor_prefixes = ['moz', 'o', 'webkit'] + + def __get_prefixed_prop(self, prefix=None): + name = self.prop.name + if prefix: + name = '-%s-%s' % (prefix, self.prop.name) + newValues = [] + for value in self.prop.value.split(','): + parts = value.strip().split(' ') + parts[0] = prefixRegex.sub('', parts[0]) + if parts[0] in rules and prefix and rules[parts[0]].should_prefix(): + parts[0] = '-%s-%s' % (prefix, parts[0]) + newValues.append(' '.join(parts)) + return cssutils.css.Property( + name=name, + value=', '.join(newValues), + priority=self.prop.priority + ) + + def get_prefixed_props(self, filt): + for prefix in [p for p in self.vendor_prefixes if p in filt]: + yield self.__get_prefixed_prop(prefix) + + def get_base_prop(self): + return self.__get_prefixed_prop() + + +class GradientReplacementRule(BaseReplacementRule): + vendor_prefixes = ['moz', 'o', 'webkit'] + + def __iter_values(self): + valueSplit = self.prop.value.split(',') + index = 0 + # currentString = '' + while(True): + if index >= len(valueSplit): + break + rawValue = valueSplit[index].strip() + snip = prefixRegex.sub('', rawValue) + if snip.startswith('linear-gradient'): + values = [re.sub('^linear-gradient\(', '', snip)] + if valueSplit[index + 1].strip().endswith(')'): + values.append(re.sub('\)+$', '', valueSplit[index + 1].strip())) + else: + values.append(valueSplit[index + 1].strip()) + values.append(re.sub('\)+$', '', valueSplit[index + 2].strip())) + if len(values) == 2: + yield { + 'start': values[0], + 'end': values[1] + } + else: + yield { + 'pos': values[0], + 'start': values[1], + 'end': values[2] + } + index += len(values) + elif snip.startswith('gradient'): + yield { + 'start': re.sub('\)+$', '', valueSplit[index + 4].strip()), + 'end': re.sub('\)+$', '', valueSplit[index + 6].strip()), + } + index += 7 + else: + # not a gradient so just yield the raw string + yield rawValue + index += 1 + + def __get_prefixed_prop(self, values, prefix=None): + gradientName = 'linear-gradient' + if prefix: + gradientName = '-%s-%s' % (prefix, gradientName) + newValues = [] + for value in values: + if isinstance(value, dict): + if 'pos' in value: + newValues.append(gradientName + '(%(pos)s, %(start)s, %(end)s)' % value) + else: + newValues.append(gradientName + '(%(start)s, %(end)s)' % value) + else: + newValues.append(value) + return cssutils.css.Property( + name=self.prop.name, + value=', '.join(newValues), + priority=self.prop.priority + ) + + def get_prefixed_props(self, filt): + values = list(self.__iter_values()) + needPrefix = False + for value in values: # check if there are any gradients + if isinstance(value, dict): + needPrefix = True + break + if needPrefix: + for prefix in [p for p in self.vendor_prefixes if p in filt]: + yield self.__get_prefixed_prop(values, prefix) + if prefix == 'webkit': + newValues = [] + for value in values: + if isinstance(value, dict): + newValues.append('-webkit-gradient(linear, left top, left bottom, color-stop(0, %(start)s), color-stop(1, %(end)s))' % value) + else: + newValues.append(value) + yield cssutils.css.Property( + name=self.prop.name, + value=', '.join(newValues), + priority=self.prop.priority + ) + else: + yield None + + def get_base_prop(self): + values = self.__iter_values() + return self.__get_prefixed_prop(values) + +rules = { + 'border-radius': BaseReplacementRule, + 'border-top-left-radius': BorderRadiusReplacementRule, + 'border-top-right-radius': BorderRadiusReplacementRule, + 'border-bottom-right-radius': BorderRadiusReplacementRule, + 'border-bottom-left-radius': BorderRadiusReplacementRule, + 'border-image': FullReplacementRule, + 'box-shadow': BaseReplacementRule, + 'box-sizing': MozReplacementRule, + 'box-orient': BaseAndIEReplacementRule, + 'box-direction': BaseAndIEReplacementRule, + 'box-ordinal-group': BaseAndIEReplacementRule, + 'box-align': BaseAndIEReplacementRule, + 'box-flex': BaseAndIEReplacementRule, + 'box-flex-group': BaseReplacementRule, + 'box-pack': BaseAndIEReplacementRule, + 'box-lines': BaseAndIEReplacementRule, + 'user-select': BaseReplacementRule, + 'user-modify': BaseReplacementRule, + 'margin-start': BaseReplacementRule, + 'margin-end': BaseReplacementRule, + 'padding-start': BaseReplacementRule, + 'padding-end': BaseReplacementRule, + 'column-count': BaseReplacementRule, + 'column-gap': BaseReplacementRule, + 'column-rule': BaseReplacementRule, + 'column-rule-color': BaseReplacementRule, + 'column-rule-style': BaseReplacementRule, + 'column-rule-width': BaseReplacementRule, + 'column-span': WebkitReplacementRule, + 'column-width': BaseReplacementRule, + 'columns': WebkitReplacementRule, + + 'background-clip': WebkitReplacementRule, + 'background-origin': WebkitReplacementRule, + 'background-size': WebkitReplacementRule, + 'background-image': GradientReplacementRule, + 'background': GradientReplacementRule, + + 'text-overflow': OperaAndIEReplacementRule, + + 'transition': TransitionReplacementRule, + 'transition-delay': BaseAndOperaReplacementRule, + 'transition-duration': BaseAndOperaReplacementRule, + 'transition-property': TransitionReplacementRule, + 'transition-timing-function': BaseAndOperaReplacementRule, + 'transform': FullReplacementRule, + 'transform-origin': FullReplacementRule, + + 'display': DisplayReplacementRule, + 'appearance': WebkitReplacementRule, + 'hyphens': BaseReplacementRule, +} diff --git a/libs/cssutils/__init__.py b/libs/cssutils/__init__.py new file mode 100755 index 00000000..8157ce57 --- /dev/null +++ b/libs/cssutils/__init__.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python +"""cssutils - CSS Cascading Style Sheets library for Python + + Copyright (C) 2004-2013 Christof Hoeke + + cssutils is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . + + +A Python package to parse and build CSS Cascading Style Sheets. DOM only, not +any rendering facilities! + +Based upon and partly implementing the following specifications : + +`CSS 2.1 `__ + General CSS rules and properties are defined here +`CSS 2.1 Errata `__ + A few errata, mainly the definition of CHARSET_SYM tokens +`CSS3 Module: Syntax `__ + Used in parts since cssutils 0.9.4. cssutils tries to use the features from + CSS 2.1 and CSS 3 with preference to CSS3 but as this is not final yet some + parts are from CSS 2.1 +`MediaQueries `__ + MediaQueries are part of ``stylesheets.MediaList`` since v0.9.4, used in + @import and @media rules. +`Namespaces `__ + Added in v0.9.1, updated to definition in CSSOM in v0.9.4, updated in 0.9.5 + for dev version +`CSS3 Module: Pages Media `__ + Most properties of this spec are implemented including MarginRules +`Selectors `__ + The selector syntax defined here (and not in CSS 2.1) should be parsable + with cssutils (*should* mind though ;) ) + +`DOM Level 2 Style CSS `__ + DOM for package css. 0.9.8 removes support for CSSValue and related API, + see PropertyValue and Value API for now +`DOM Level 2 Style Stylesheets `__ + DOM for package stylesheets +`CSSOM `__ + A few details (mainly the NamespaceRule DOM) is taken from here. Plan is + to move implementation to the stuff defined here which is newer but still + no REC so might change anytime... + + +The cssutils tokenizer is a customized implementation of `CSS3 Module: Syntax +(W3C Working Draft 13 August 2003) `__ which +itself is based on the CSS 2.1 tokenizer. It tries to be as compliant as +possible but uses some (helpful) parts of the CSS 2.1 tokenizer. + +I guess cssutils is neither CSS 2.1 nor CSS 3 compliant but tries to at least +be able to parse both grammars including some more real world cases (some CSS +hacks are actually parsed and serialized). Both official grammars are not final +nor bugfree but still feasible. cssutils aim is not to be fully compliant to +any CSS specification (the specifications seem to be in a constant flow anyway) +but cssutils *should* be able to read and write as many as possible CSS +stylesheets "in the wild" while at the same time implement the official APIs +which are well documented. Some minor extensions are provided as well. + +Please visit http://cthedot.de/cssutils/ for more details. + + +Tested with Python 2.7.3 and 3.3 on Windows 8 64bit. + + +This library may be used ``from cssutils import *`` which +import subpackages ``css`` and ``stylesheets``, CSSParser and +CSSSerializer classes only. + +Usage may be:: + + >>> from cssutils import * + >>> parser = CSSParser() + >>> sheet = parser.parseString(u'a { color: red}') + >>> print sheet.cssText + a { + color: red + } + +""" +__all__ = ['css', 'stylesheets', 'CSSParser', 'CSSSerializer'] +__docformat__ = 'restructuredtext' +__author__ = 'Christof Hoeke with contributions by Walter Doerwald' +__date__ = '$LastChangedDate:: $:' + +VERSION = '0.9.10' + +__version__ = '%s $Id$' % VERSION + +import sys +if sys.version_info < (2,6): + bytes = str + +import codec +import os.path +import urllib +import urlparse +import xml.dom + +# order of imports is important (partly circular) +from . import util +import errorhandler +log = errorhandler.ErrorHandler() + +import css +import stylesheets +from parse import CSSParser + +from serialize import CSSSerializer +ser = CSSSerializer() + +from profiles import Profiles +profile = Profiles(log=log) + +# used by Selector defining namespace prefix '*' +_ANYNS = -1 + +class DOMImplementationCSS(object): + """This interface allows the DOM user to create a CSSStyleSheet + outside the context of a document. There is no way to associate + the new CSSStyleSheet with a document in DOM Level 2. + + This class is its *own factory*, as it is given to + xml.dom.registerDOMImplementation which simply calls it and receives + an instance of this class then. + """ + _features = [ + ('css', '1.0'), + ('css', '2.0'), + ('stylesheets', '1.0'), + ('stylesheets', '2.0') + ] + + def createCSSStyleSheet(self, title, media): + """ + Creates a new CSSStyleSheet. + + title of type DOMString + The advisory title. See also the Style Sheet Interfaces + section. + media of type DOMString + The comma-separated list of media associated with the new style + sheet. See also the Style Sheet Interfaces section. + + returns + CSSStyleSheet: A new CSS style sheet. + + TODO: DOMException + SYNTAX_ERR: Raised if the specified media string value has a + syntax error and is unparsable. + """ + return css.CSSStyleSheet(title=title, media=media) + + def createDocument(self, *args): + # not needed to HTML, also not for CSS? + raise NotImplementedError + + def createDocumentType(self, *args): + # not needed to HTML, also not for CSS? + raise NotImplementedError + + def hasFeature(self, feature, version): + return (feature.lower(), unicode(version)) in self._features + +xml.dom.registerDOMImplementation('cssutils', DOMImplementationCSS) + + +def parseString(*a, **k): + return CSSParser().parseString(*a, **k) +parseString.__doc__ = CSSParser.parseString.__doc__ + +def parseFile(*a, **k): + return CSSParser().parseFile(*a, **k) +parseFile.__doc__ = CSSParser.parseFile.__doc__ + +def parseUrl(*a, **k): + return CSSParser().parseUrl(*a, **k) +parseUrl.__doc__ = CSSParser.parseUrl.__doc__ + +def parseStyle(*a, **k): + return CSSParser().parseStyle(*a, **k) +parseStyle.__doc__ = CSSParser.parseStyle.__doc__ + +# set "ser", default serializer +def setSerializer(serializer): + """Set the global serializer used by all class in cssutils.""" + global ser + ser = serializer + +def getUrls(sheet): + """Retrieve all ``url(urlstring)`` values (in e.g. + :class:`cssutils.css.CSSImportRule` or :class:`cssutils.css.CSSValue` + objects of given `sheet`. + + :param sheet: + :class:`cssutils.css.CSSStyleSheet` object whose URLs are yielded + + This function is a generator. The generated URL values exclude ``url(`` and + ``)`` and surrounding single or double quotes. + """ + for importrule in (r for r in sheet if r.type == r.IMPORT_RULE): + yield importrule.href + + def styleDeclarations(base): + "recursive generator to find all CSSStyleDeclarations" + if hasattr(base, 'cssRules'): + for rule in base.cssRules: + for s in styleDeclarations(rule): + yield s + elif hasattr(base, 'style'): + yield base.style + + for style in styleDeclarations(sheet): + for p in style.getProperties(all=True): + for v in p.propertyValue: + if v.type == 'URI': + yield v.uri + +def replaceUrls(sheetOrStyle, replacer, ignoreImportRules=False): + """Replace all URLs in :class:`cssutils.css.CSSImportRule` or + :class:`cssutils.css.CSSValue` objects of given `sheetOrStyle`. + + :param sheetOrStyle: + a :class:`cssutils.css.CSSStyleSheet` or a + :class:`cssutils.css.CSSStyleDeclaration` which is changed in place + :param replacer: + a function which is called with a single argument `url` which + is the current value of each url() excluding ``url(``, ``)`` and + surrounding (single or double) quotes. + :param ignoreImportRules: + if ``True`` does not call `replacer` with URLs from @import rules. + """ + if not ignoreImportRules and not isinstance(sheetOrStyle, + css.CSSStyleDeclaration): + for importrule in (r for r in sheetOrStyle if r.type == r.IMPORT_RULE): + importrule.href = replacer(importrule.href) + + def styleDeclarations(base): + "recursive generator to find all CSSStyleDeclarations" + if hasattr(base, 'cssRules'): + for rule in base.cssRules: + for s in styleDeclarations(rule): + yield s + elif hasattr(base, 'style'): + yield base.style + elif isinstance(sheetOrStyle, css.CSSStyleDeclaration): + # base is a style already + yield base + + for style in styleDeclarations(sheetOrStyle): + for p in style.getProperties(all=True): + for v in p.propertyValue: + if v.type == v.URI: + v.uri = replacer(v.uri) + +def resolveImports(sheet, target=None): + """Recurcively combine all rules in given `sheet` into a `target` sheet. + @import rules which use media information are tried to be wrapped into + @media rules so keeping the media information. This may not work in + all instances (if e.g. an @import rule itself contains an @import rule + with different media infos or if it contains rules which may not be + used inside an @media block like @namespace rules.). In these cases + the @import rule is kept as in the original sheet and a WARNING is issued. + + :param sheet: + in this given :class:`cssutils.css.CSSStyleSheet` all import rules are + resolved and added to a resulting *flat* sheet. + :param target: + A :class:`cssutils.css.CSSStyleSheet` object which will be the + resulting *flat* sheet if given + :returns: given `target` or a new :class:`cssutils.css.CSSStyleSheet` + object + """ + if not target: + target = css.CSSStyleSheet(href=sheet.href, + media=sheet.media, + title=sheet.title) + + def getReplacer(targetbase): + "Return a replacer which uses base to return adjusted URLs" + basesch, baseloc, basepath, basequery, basefrag = urlparse.urlsplit(targetbase) + basepath, basepathfilename = os.path.split(basepath) + + def replacer(uri): + scheme, location, path, query, fragment = urlparse.urlsplit(uri) + if not scheme and not location and not path.startswith(u'/'): + # relative + path, filename = os.path.split(path) + combined = os.path.normpath(os.path.join(basepath, path, filename)) + return urllib.pathname2url(combined) + else: + # keep anything absolute + return uri + + return replacer + + for rule in sheet.cssRules: + if rule.type == rule.CHARSET_RULE: + pass + elif rule.type == rule.IMPORT_RULE: + log.info(u'Processing @import %r' % rule.href, neverraise=True) + + if rule.hrefFound: + # add all rules of @import to current sheet + target.add(css.CSSComment(cssText=u'/* START @import "%s" */' + % rule.href)) + + try: + # nested imports + importedSheet = resolveImports(rule.styleSheet) + except xml.dom.HierarchyRequestErr, e: + log.warn(u'@import: Cannot resolve target, keeping rule: %s' + % e, neverraise=True) + target.add(rule) + else: + # adjust relative URI references + log.info(u'@import: Adjusting paths for %r' % rule.href, + neverraise=True) + replaceUrls(importedSheet, + getReplacer(rule.href), + ignoreImportRules=True) + + # might have to wrap rules in @media if media given + if rule.media.mediaText == u'all': + mediaproxy = None + else: + keepimport = False + for r in importedSheet: + # check if rules present which may not be + # combined with media + if r.type not in (r.COMMENT, + r.STYLE_RULE, + r.IMPORT_RULE): + keepimport = True + break + if keepimport: + log.warn(u'Cannot combine imported sheet with' + u' given media as other rules then' + u' comments or stylerules found %r,' + u' keeping %r' % (r, + rule.cssText), + neverraise=True) + target.add(rule) + continue + + # wrap in @media if media is not `all` + log.info(u'@import: Wrapping some rules in @media ' + u' to keep media: %s' + % rule.media.mediaText, neverraise=True) + mediaproxy = css.CSSMediaRule(rule.media.mediaText) + + for r in importedSheet: + if mediaproxy: + mediaproxy.add(r) + else: + # add to top sheet directly but are difficult anyway + target.add(r) + + if mediaproxy: + target.add(mediaproxy) + + else: + # keep @import as it is + log.error(u'Cannot get referenced stylesheet %r, keeping rule' + % rule.href, neverraise=True) + target.add(rule) + + else: + target.add(rule) + + return target + + +if __name__ == '__main__': + print __doc__ diff --git a/libs/cssutils/_codec2.py b/libs/cssutils/_codec2.py new file mode 100755 index 00000000..d0ae617f --- /dev/null +++ b/libs/cssutils/_codec2.py @@ -0,0 +1,584 @@ +#!/usr/bin/env python +"""Python codec for CSS.""" +__docformat__ = 'restructuredtext' +__author__ = 'Walter Doerwald' +__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $' + +import codecs +import marshal + +# We're using bits to store all possible candidate encodings (or variants, i.e. +# we have two bits for the variants of UTF-16 and two for the +# variants of UTF-32). +# +# Prefixes for various CSS encodings +# UTF-8-SIG xEF xBB xBF +# UTF-16 (LE) xFF xFE ~x00|~x00 +# UTF-16 (BE) xFE xFF +# UTF-16-LE @ x00 @ x00 +# UTF-16-BE x00 @ +# UTF-32 (LE) xFF xFE x00 x00 +# UTF-32 (BE) x00 x00 xFE xFF +# UTF-32-LE @ x00 x00 x00 +# UTF-32-BE x00 x00 x00 @ +# CHARSET @ c h a ... + + +def detectencoding_str(input, final=False): + """ + Detect the encoding of the byte string ``input``, which contains the + beginning of a CSS file. This function returns the detected encoding (or + ``None`` if it hasn't got enough data), and a flag that indicates whether + that encoding has been detected explicitely or implicitely. To detect the + encoding the first few bytes are used (or if ``input`` is ASCII compatible + and starts with a charset rule the encoding name from the rule). "Explicit" + detection means that the bytes start with a BOM or a charset rule. + + If the encoding can't be detected yet, ``None`` is returned as the encoding. + ``final`` specifies whether more data will be available in later calls or + not. If ``final`` is true, ``detectencoding_str()`` will never return + ``None`` as the encoding. + """ + + # A bit for every candidate + CANDIDATE_UTF_8_SIG = 1 + CANDIDATE_UTF_16_AS_LE = 2 + CANDIDATE_UTF_16_AS_BE = 4 + CANDIDATE_UTF_16_LE = 8 + CANDIDATE_UTF_16_BE = 16 + CANDIDATE_UTF_32_AS_LE = 32 + CANDIDATE_UTF_32_AS_BE = 64 + CANDIDATE_UTF_32_LE = 128 + CANDIDATE_UTF_32_BE = 256 + CANDIDATE_CHARSET = 512 + + candidates = 1023 # all candidates + + li = len(input) + if li>=1: + # Check first byte + c = input[0] + if c != "\xef": + candidates &= ~CANDIDATE_UTF_8_SIG + if c != "\xff": + candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_16_AS_LE) + if c != "\xfe": + candidates &= ~CANDIDATE_UTF_16_AS_BE + if c != "@": + candidates &= ~(CANDIDATE_UTF_32_LE|CANDIDATE_UTF_16_LE|CANDIDATE_CHARSET) + if c != "\x00": + candidates &= ~(CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_BE|CANDIDATE_UTF_16_BE) + if li>=2: + # Check second byte + c = input[1] + if c != "\xbb": + candidates &= ~CANDIDATE_UTF_8_SIG + if c != "\xfe": + candidates &= ~(CANDIDATE_UTF_16_AS_LE|CANDIDATE_UTF_32_AS_LE) + if c != "\xff": + candidates &= ~CANDIDATE_UTF_16_AS_BE + if c != "\x00": + candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE) + if c != "@": + candidates &= ~CANDIDATE_UTF_16_BE + if c != "c": + candidates &= ~CANDIDATE_CHARSET + if li>=3: + # Check third byte + c = input[2] + if c != "\xbf": + candidates &= ~CANDIDATE_UTF_8_SIG + if c != "c": + candidates &= ~CANDIDATE_UTF_16_LE + if c != "\x00": + candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE) + if c != "\xfe": + candidates &= ~CANDIDATE_UTF_32_AS_BE + if c != "h": + candidates &= ~CANDIDATE_CHARSET + if li>=4: + # Check fourth byte + c = input[3] + if input[2:4] == "\x00\x00": + candidates &= ~CANDIDATE_UTF_16_AS_LE + if c != "\x00": + candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE) + if c != "\xff": + candidates &= ~CANDIDATE_UTF_32_AS_BE + if c != "@": + candidates &= ~CANDIDATE_UTF_32_BE + if c != "a": + candidates &= ~CANDIDATE_CHARSET + if candidates == 0: + return ("utf-8", False) + if not (candidates & (candidates-1)): # only one candidate remaining + if candidates == CANDIDATE_UTF_8_SIG and li >= 3: + return ("utf-8-sig", True) + elif candidates == CANDIDATE_UTF_16_AS_LE and li >= 2: + return ("utf-16", True) + elif candidates == CANDIDATE_UTF_16_AS_BE and li >= 2: + return ("utf-16", True) + elif candidates == CANDIDATE_UTF_16_LE and li >= 4: + return ("utf-16-le", False) + elif candidates == CANDIDATE_UTF_16_BE and li >= 2: + return ("utf-16-be", False) + elif candidates == CANDIDATE_UTF_32_AS_LE and li >= 4: + return ("utf-32", True) + elif candidates == CANDIDATE_UTF_32_AS_BE and li >= 4: + return ("utf-32", True) + elif candidates == CANDIDATE_UTF_32_LE and li >= 4: + return ("utf-32-le", False) + elif candidates == CANDIDATE_UTF_32_BE and li >= 4: + return ("utf-32-be", False) + elif candidates == CANDIDATE_CHARSET and li >= 4: + prefix = '@charset "' + if input[:len(prefix)] == prefix: + pos = input.find('"', len(prefix)) + if pos >= 0: + return (input[len(prefix):pos], True) + # if this is the last call, and we haven't determined an encoding yet, + # we default to UTF-8 + if final: + return ("utf-8", False) + return (None, False) # dont' know yet + + +def detectencoding_unicode(input, final=False): + """ + Detect the encoding of the unicode string ``input``, which contains the + beginning of a CSS file. The encoding is detected from the charset rule + at the beginning of ``input``. If there is no charset rule, ``"utf-8"`` + will be returned. + + If the encoding can't be detected yet, ``None`` is returned. ``final`` + specifies whether more data will be available in later calls or not. If + ``final`` is true, ``detectencoding_unicode()`` will never return ``None``. + """ + prefix = u'@charset "' + if input.startswith(prefix): + pos = input.find(u'"', len(prefix)) + if pos >= 0: + return (input[len(prefix):pos], True) + elif final or not prefix.startswith(input): + # if this is the last call, and we haven't determined an encoding yet, + # (or the string definitely doesn't start with prefix) we default to UTF-8 + return ("utf-8", False) + return (None, False) # don't know yet + + +def _fixencoding(input, encoding, final=False): + """ + Replace the name of the encoding in the charset rule at the beginning of + ``input`` with ``encoding``. If ``input`` doesn't starts with a charset + rule, ``input`` will be returned unmodified. + + If the encoding can't be found yet, ``None`` is returned. ``final`` + specifies whether more data will be available in later calls or not. + If ``final`` is true, ``_fixencoding()`` will never return ``None``. + """ + prefix = u'@charset "' + if len(input) > len(prefix): + if input.startswith(prefix): + pos = input.find(u'"', len(prefix)) + if pos >= 0: + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = u"utf-8" + return prefix + encoding + input[pos:] + # we haven't seen the end of the encoding name yet => fall through + else: + return input # doesn't start with prefix, so nothing to fix + elif not prefix.startswith(input) or final: + # can't turn out to be a @charset rule later (or there is no "later") + return input + if final: + return input + return None # don't know yet + + +def decode(input, errors="strict", encoding=None, force=True): + if encoding is None or not force: + (_encoding, explicit) = detectencoding_str(input, True) + if _encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not force) or encoding is None: # Take the encoding from the input + encoding = _encoding + (input, consumed) = codecs.getdecoder(encoding)(input, errors) + return (_fixencoding(input, unicode(encoding), True), consumed) + + +def encode(input, errors="strict", encoding=None): + consumed = len(input) + if encoding is None: + encoding = detectencoding_unicode(input, True)[0] + if encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, u"utf-8", True) + else: + input = _fixencoding(input, unicode(encoding), True) + if encoding == "css": + raise ValueError("css not allowed as encoding name") + encoder = codecs.getencoder(encoding) + return (encoder(input, errors)[0], consumed) + + +def _bytes2int(bytes): + # Helper: convert an 8 bit string into an ``int``. + i = 0 + for byte in bytes: + i = (i<<8) + ord(byte) + return i + + +def _int2bytes(i): + # Helper: convert an ``int`` into an 8-bit string. + v = [] + while i: + v.insert(0, chr(i&0xff)) + i >>= 8 + return "".join(v) + + +if hasattr(codecs, "IncrementalDecoder"): + class IncrementalDecoder(codecs.IncrementalDecoder): + def __init__(self, errors="strict", encoding=None, force=True): + self.decoder = None + self.encoding = encoding + self.force = force + codecs.IncrementalDecoder.__init__(self, errors) + # Store ``errors`` somewhere else, + # because we have to hide it in a property + self._errors = errors + self.buffer = u"".encode() + self.headerfixed = False + + def iterdecode(self, input): + for part in input: + result = self.decode(part, False) + if result: + yield result + result = self.decode("", True) + if result: + yield result + + def decode(self, input, final=False): + # We're doing basically the same as a ``BufferedIncrementalDecoder``, + # but since the buffer is only relevant until the encoding has been + # detected (in which case the buffer of the underlying codec might + # kick in), we're implementing buffering ourselves to avoid some + # overhead. + if self.decoder is None: + input = self.buffer + input + # Do we have to detect the encoding from the input? + if self.encoding is None or not self.force: + (encoding, explicit) = detectencoding_str(input, final) + if encoding is None: # no encoding determined yet + self.buffer = input # retry the complete input on the next call + return u"" # no encoding determined yet, so no output + elif encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input + self.encoding = encoding + self.buffer = "" # drop buffer, as the decoder might keep its own + decoder = codecs.getincrementaldecoder(self.encoding) + self.decoder = decoder(self._errors) + if self.headerfixed: + return self.decoder.decode(input, final) + # If we haven't fixed the header yet, + # the content of ``self.buffer`` is a ``unicode`` object + output = self.buffer + self.decoder.decode(input, final) + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newoutput = _fixencoding(output, unicode(encoding), final) + if newoutput is None: + # retry fixing the @charset rule (but keep the decoded stuff) + self.buffer = output + return u"" + self.headerfixed = True + return newoutput + + def reset(self): + codecs.IncrementalDecoder.reset(self) + self.decoder = None + self.buffer = u"".encode() + self.headerfixed = False + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the real decoder too + if self.decoder is not None: + self.decoder.errors = errors + self._errors = errors + errors = property(_geterrors, _seterrors) + + def getstate(self): + if self.decoder is not None: + state = (self.encoding, self.buffer, self.headerfixed, True, self.decoder.getstate()) + else: + state = (self.encoding, self.buffer, self.headerfixed, False, None) + return ("", _bytes2int(marshal.dumps(state))) + + def setstate(self, state): + state = _int2bytes(marshal.loads(state[1])) # ignore buffered input + self.encoding = state[0] + self.buffer = state[1] + self.headerfixed = state[2] + if state[3] is not None: + self.decoder = codecs.getincrementaldecoder(self.encoding)(self._errors) + self.decoder.setstate(state[4]) + else: + self.decoder = None + + +if hasattr(codecs, "IncrementalEncoder"): + class IncrementalEncoder(codecs.IncrementalEncoder): + def __init__(self, errors="strict", encoding=None): + self.encoder = None + self.encoding = encoding + codecs.IncrementalEncoder.__init__(self, errors) + # Store ``errors`` somewhere else, + # because we have to hide it in a property + self._errors = errors + self.buffer = u"" + + def iterencode(self, input): + for part in input: + result = self.encode(part, False) + if result: + yield result + result = self.encode(u"", True) + if result: + yield result + + def encode(self, input, final=False): + if self.encoder is None: + input = self.buffer + input + if self.encoding is not None: + # Replace encoding in the @charset rule with the specified one + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newinput = _fixencoding(input, unicode(encoding), final) + if newinput is None: # @charset rule incomplete => Retry next time + self.buffer = input + return "" + input = newinput + else: + # Use encoding from the @charset declaration + self.encoding = detectencoding_unicode(input, final)[0] + if self.encoding is not None: + if self.encoding == "css": + raise ValueError("css not allowed as encoding name") + info = codecs.lookup(self.encoding) + encoding = self.encoding + if self.encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, u"utf-8", True) + self.encoder = info.incrementalencoder(self._errors) + self.buffer = u"" + else: + self.buffer = input + return "" + return self.encoder.encode(input, final) + + def reset(self): + codecs.IncrementalEncoder.reset(self) + self.encoder = None + self.buffer = u"" + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors ``must be done on the real encoder too + if self.encoder is not None: + self.encoder.errors = errors + self._errors = errors + errors = property(_geterrors, _seterrors) + + def getstate(self): + if self.encoder is not None: + state = (self.encoding, self.buffer, True, self.encoder.getstate()) + else: + state = (self.encoding, self.buffer, False, None) + return _bytes2int(marshal.dumps(state)) + + def setstate(self, state): + state = _int2bytes(marshal.loads(state)) + self.encoding = state[0] + self.buffer = state[1] + if state[2] is not None: + self.encoder = codecs.getincrementalencoder(self.encoding)(self._errors) + self.encoder.setstate(state[4]) + else: + self.encoder = None + + +class StreamWriter(codecs.StreamWriter): + def __init__(self, stream, errors="strict", encoding=None, header=False): + codecs.StreamWriter.__init__(self, stream, errors) + self.streamwriter = None + self.encoding = encoding + self._errors = errors + self.buffer = u"" + + def encode(self, input, errors='strict'): + li = len(input) + if self.streamwriter is None: + input = self.buffer + input + li = len(input) + if self.encoding is not None: + # Replace encoding in the @charset rule with the specified one + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newinput = _fixencoding(input, unicode(encoding), False) + if newinput is None: # @charset rule incomplete => Retry next time + self.buffer = input + return ("", 0) + input = newinput + else: + # Use encoding from the @charset declaration + self.encoding = detectencoding_unicode(input, False)[0] + if self.encoding is not None: + if self.encoding == "css": + raise ValueError("css not allowed as encoding name") + self.streamwriter = codecs.getwriter(self.encoding)(self.stream, self._errors) + encoding = self.encoding + if self.encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, u"utf-8", True) + self.buffer = u"" + else: + self.buffer = input + return ("", 0) + return (self.streamwriter.encode(input, errors)[0], li) + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the streamwriter too + if self.streamwriter is not None: + self.streamwriter.errors = errors + self._errors = errors + + errors = property(_geterrors, _seterrors) + + +class StreamReader(codecs.StreamReader): + def __init__(self, stream, errors="strict", encoding=None, force=True): + codecs.StreamReader.__init__(self, stream, errors) + self.streamreader = None + self.encoding = encoding + self.force = force + self._errors = errors + + def decode(self, input, errors='strict'): + if self.streamreader is None: + if self.encoding is None or not self.force: + (encoding, explicit) = detectencoding_str(input, False) + if encoding is None: # no encoding determined yet + return (u"", 0) # no encoding determined yet, so no output + elif encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input + self.encoding = encoding + streamreader = codecs.getreader(self.encoding) + streamreader = streamreader(self.stream, self._errors) + (output, consumed) = streamreader.decode(input, errors) + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newoutput = _fixencoding(output, unicode(encoding), False) + if newoutput is not None: + self.streamreader = streamreader + return (newoutput, consumed) + return (u"", 0) # we will create a new streamreader on the next call + return self.streamreader.decode(input, errors) + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the streamreader too + if self.streamreader is not None: + self.streamreader.errors = errors + self._errors = errors + + errors = property(_geterrors, _seterrors) + + +if hasattr(codecs, "CodecInfo"): + # We're running on Python 2.5 or better + def search_function(name): + if name == "css": + return codecs.CodecInfo( + name="css", + encode=encode, + decode=decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + ) +else: + # If we're running on Python 2.4, define the utf-8-sig codec here + def utf8sig_encode(input, errors='strict'): + return (codecs.BOM_UTF8 + codecs.utf_8_encode(input, errors)[0], len(input)) + + def utf8sig_decode(input, errors='strict'): + prefix = 0 + if input[:3] == codecs.BOM_UTF8: + input = input[3:] + prefix = 3 + (output, consumed) = codecs.utf_8_decode(input, errors, True) + return (output, consumed+prefix) + + class UTF8SigStreamWriter(codecs.StreamWriter): + def reset(self): + codecs.StreamWriter.reset(self) + try: + del self.encode + except AttributeError: + pass + + def encode(self, input, errors='strict'): + self.encode = codecs.utf_8_encode + return utf8sig_encode(input, errors) + + class UTF8SigStreamReader(codecs.StreamReader): + def reset(self): + codecs.StreamReader.reset(self) + try: + del self.decode + except AttributeError: + pass + + def decode(self, input, errors='strict'): + if len(input) < 3 and codecs.BOM_UTF8.startswith(input): + # not enough data to decide if this is a BOM + # => try again on the next call + return (u"", 0) + self.decode = codecs.utf_8_decode + return utf8sig_decode(input, errors) + + def search_function(name): + import encodings + name = encodings.normalize_encoding(name) + if name == "css": + return (encode, decode, StreamReader, StreamWriter) + elif name == "utf_8_sig": + return (utf8sig_encode, utf8sig_decode, UTF8SigStreamReader, UTF8SigStreamWriter) + + +codecs.register(search_function) + + +# Error handler for CSS escaping + +def cssescape(exc): + if not isinstance(exc, UnicodeEncodeError): + raise TypeError("don't know how to handle %r" % exc) + return (u"".join(u"\\%06x" % ord(c) for c in exc.object[exc.start:exc.end]), exc.end) + +codecs.register_error("cssescape", cssescape) diff --git a/libs/cssutils/_codec3.py b/libs/cssutils/_codec3.py new file mode 100755 index 00000000..878a7b29 --- /dev/null +++ b/libs/cssutils/_codec3.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python +"""Python codec for CSS.""" +__docformat__ = 'restructuredtext' +__author__ = 'Walter Doerwald' +__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $' + +import sys +import codecs +import marshal + +# We're using bits to store all possible candidate encodings (or variants, i.e. +# we have two bits for the variants of UTF-16 and two for the +# variants of UTF-32). +# +# Prefixes for various CSS encodings +# UTF-8-SIG xEF xBB xBF +# UTF-16 (LE) xFF xFE ~x00|~x00 +# UTF-16 (BE) xFE xFF +# UTF-16-LE @ x00 @ x00 +# UTF-16-BE x00 @ +# UTF-32 (LE) xFF xFE x00 x00 +# UTF-32 (BE) x00 x00 xFE xFF +# UTF-32-LE @ x00 x00 x00 +# UTF-32-BE x00 x00 x00 @ +# CHARSET @ c h a ... + + +def chars(bytestring): + return ''.join(chr(byte) for byte in bytestring) + + +def detectencoding_str(input, final=False): + """ + Detect the encoding of the byte string ``input``, which contains the + beginning of a CSS file. This function returns the detected encoding (or + ``None`` if it hasn't got enough data), and a flag that indicates whether + that encoding has been detected explicitely or implicitely. To detect the + encoding the first few bytes are used (or if ``input`` is ASCII compatible + and starts with a charset rule the encoding name from the rule). "Explicit" + detection means that the bytes start with a BOM or a charset rule. + + If the encoding can't be detected yet, ``None`` is returned as the encoding. + ``final`` specifies whether more data will be available in later calls or + not. If ``final`` is true, ``detectencoding_str()`` will never return + ``None`` as the encoding. + """ + + # A bit for every candidate + CANDIDATE_UTF_8_SIG = 1 + CANDIDATE_UTF_16_AS_LE = 2 + CANDIDATE_UTF_16_AS_BE = 4 + CANDIDATE_UTF_16_LE = 8 + CANDIDATE_UTF_16_BE = 16 + CANDIDATE_UTF_32_AS_LE = 32 + CANDIDATE_UTF_32_AS_BE = 64 + CANDIDATE_UTF_32_LE = 128 + CANDIDATE_UTF_32_BE = 256 + CANDIDATE_CHARSET = 512 + + candidates = 1023 # all candidates + + #input = chars(input) + li = len(input) + if li>=1: + # Check first byte + c = input[0] + if c != b"\xef"[0]: + candidates &= ~CANDIDATE_UTF_8_SIG + if c != b"\xff"[0]: + candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_16_AS_LE) + if c != b"\xfe"[0]: + candidates &= ~CANDIDATE_UTF_16_AS_BE + if c != b"@"[0]: + candidates &= ~(CANDIDATE_UTF_32_LE|CANDIDATE_UTF_16_LE|CANDIDATE_CHARSET) + if c != b"\x00"[0]: + candidates &= ~(CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_BE|CANDIDATE_UTF_16_BE) + if li>=2: + # Check second byte + c = input[1] + if c != b"\xbb"[0]: + candidates &= ~CANDIDATE_UTF_8_SIG + if c != b"\xfe"[0]: + candidates &= ~(CANDIDATE_UTF_16_AS_LE|CANDIDATE_UTF_32_AS_LE) + if c != b"\xff"[0]: + candidates &= ~CANDIDATE_UTF_16_AS_BE + if c != b"\x00"[0]: + candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_BE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE) + if c != b"@"[0]: + candidates &= ~CANDIDATE_UTF_16_BE + if c != b"c"[0]: + candidates &= ~CANDIDATE_CHARSET + if li>=3: + # Check third byte + c = input[2] + if c != b"\xbf"[0]: + candidates &= ~CANDIDATE_UTF_8_SIG + if c != b"c"[0]: + candidates &= ~CANDIDATE_UTF_16_LE + if c != b"\x00"[0]: + candidates &= ~(CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE|CANDIDATE_UTF_32_BE) + if c != b"\xfe"[0]: + candidates &= ~CANDIDATE_UTF_32_AS_BE + if c != b"h"[0]: + candidates &= ~CANDIDATE_CHARSET + if li>=4: + # Check fourth byte + c = input[3] + if input[2:4] == b"\x00\x00"[0:2]: + candidates &= ~CANDIDATE_UTF_16_AS_LE + if c != b"\x00"[0]: + candidates &= ~(CANDIDATE_UTF_16_LE|CANDIDATE_UTF_32_AS_LE|CANDIDATE_UTF_32_LE) + if c != b"\xff"[0]: + candidates &= ~CANDIDATE_UTF_32_AS_BE + if c != b"@"[0]: + candidates &= ~CANDIDATE_UTF_32_BE + if c != b"a"[0]: + candidates &= ~CANDIDATE_CHARSET + if candidates == 0: + return ("utf-8", False) + if not (candidates & (candidates-1)): # only one candidate remaining + if candidates == CANDIDATE_UTF_8_SIG and li >= 3: + return ("utf-8-sig", True) + elif candidates == CANDIDATE_UTF_16_AS_LE and li >= 2: + return ("utf-16", True) + elif candidates == CANDIDATE_UTF_16_AS_BE and li >= 2: + return ("utf-16", True) + elif candidates == CANDIDATE_UTF_16_LE and li >= 4: + return ("utf-16-le", False) + elif candidates == CANDIDATE_UTF_16_BE and li >= 2: + return ("utf-16-be", False) + elif candidates == CANDIDATE_UTF_32_AS_LE and li >= 4: + return ("utf-32", True) + elif candidates == CANDIDATE_UTF_32_AS_BE and li >= 4: + return ("utf-32", True) + elif candidates == CANDIDATE_UTF_32_LE and li >= 4: + return ("utf-32-le", False) + elif candidates == CANDIDATE_UTF_32_BE and li >= 4: + return ("utf-32-be", False) + elif candidates == CANDIDATE_CHARSET and li >= 4: + prefix = '@charset "' + charsinput = chars(input) + if charsinput[:len(prefix)] == prefix: + pos = charsinput.find('"', len(prefix)) + if pos >= 0: + # TODO: return str and not bytes! + return (charsinput[len(prefix):pos], True) + # if this is the last call, and we haven't determined an encoding yet, + # we default to UTF-8 + if final: + return ("utf-8", False) + return (None, False) # dont' know yet + + +def detectencoding_unicode(input, final=False): + """ + Detect the encoding of the unicode string ``input``, which contains the + beginning of a CSS file. The encoding is detected from the charset rule + at the beginning of ``input``. If there is no charset rule, ``"utf-8"`` + will be returned. + + If the encoding can't be detected yet, ``None`` is returned. ``final`` + specifies whether more data will be available in later calls or not. If + ``final`` is true, ``detectencoding_unicode()`` will never return ``None``. + """ + prefix = '@charset "' + if input.startswith(prefix): + pos = input.find('"', len(prefix)) + if pos >= 0: + return (input[len(prefix):pos], True) + elif final or not prefix.startswith(input): + # if this is the last call, and we haven't determined an encoding yet, + # (or the string definitely doesn't start with prefix) we default to UTF-8 + return ("utf-8", False) + return (None, False) # don't know yet + + +def _fixencoding(input, encoding, final=False): + """ + Replace the name of the encoding in the charset rule at the beginning of + ``input`` with ``encoding``. If ``input`` doesn't starts with a charset + rule, ``input`` will be returned unmodified. + + If the encoding can't be found yet, ``None`` is returned. ``final`` + specifies whether more data will be available in later calls or not. + If ``final`` is true, ``_fixencoding()`` will never return ``None``. + """ + prefix = '@charset "' + if len(input) > len(prefix): + if input.startswith(prefix): + pos = input.find('"', len(prefix)) + if pos >= 0: + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + return prefix + encoding + input[pos:] + # we haven't seen the end of the encoding name yet => fall through + else: + return input # doesn't start with prefix, so nothing to fix + elif not prefix.startswith(input) or final: + # can't turn out to be a @charset rule later (or there is no "later") + return input + if final: + return input + return None # don't know yet + + +def decode(input, errors="strict", encoding=None, force=True): + try: + # py 3 only, memory?! object to bytes + input = input.tobytes() + except AttributeError as e: + pass + + if encoding is None or not force: + (_encoding, explicit) = detectencoding_str(input, True) + if _encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not force) or encoding is None: # Take the encoding from the input + encoding = _encoding + + # NEEDS: change in parse.py (str to bytes!) + (input, consumed) = codecs.getdecoder(encoding)(input, errors) + return (_fixencoding(input, str(encoding), True), consumed) + + +def encode(input, errors="strict", encoding=None): + consumed = len(input) + if encoding is None: + encoding = detectencoding_unicode(input, True)[0] + if encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, "utf-8", True) + else: + input = _fixencoding(input, str(encoding), True) + if encoding == "css": + raise ValueError("css not allowed as encoding name") + encoder = codecs.getencoder(encoding) + return (encoder(input, errors)[0], consumed) + + +def _bytes2int(bytes): + # Helper: convert an 8 bit string into an ``int``. + i = 0 + for byte in bytes: + i = (i<<8) + ord(byte) + return i + + +def _int2bytes(i): + # Helper: convert an ``int`` into an 8-bit string. + v = [] + while i: + v.insert(0, chr(i&0xff)) + i >>= 8 + return "".join(v) + + +if hasattr(codecs, "IncrementalDecoder"): + class IncrementalDecoder(codecs.IncrementalDecoder): + def __init__(self, errors="strict", encoding=None, force=True): + self.decoder = None + self.encoding = encoding + self.force = force + codecs.IncrementalDecoder.__init__(self, errors) + # Store ``errors`` somewhere else, + # because we have to hide it in a property + self._errors = errors + self.buffer = b"" + self.headerfixed = False + + def iterdecode(self, input): + for part in input: + result = self.decode(part, False) + if result: + yield result + result = self.decode("", True) + if result: + yield result + + def decode(self, input, final=False): + # We're doing basically the same as a ``BufferedIncrementalDecoder``, + # but since the buffer is only relevant until the encoding has been + # detected (in which case the buffer of the underlying codec might + # kick in), we're implementing buffering ourselves to avoid some + # overhead. + if self.decoder is None: + input = self.buffer + input + # Do we have to detect the encoding from the input? + if self.encoding is None or not self.force: + (encoding, explicit) = detectencoding_str(input, final) + if encoding is None: # no encoding determined yet + self.buffer = input # retry the complete input on the next call + return "" # no encoding determined yet, so no output + elif encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input + self.encoding = encoding + self.buffer = "" # drop buffer, as the decoder might keep its own + decoder = codecs.getincrementaldecoder(self.encoding) + self.decoder = decoder(self._errors) + if self.headerfixed: + return self.decoder.decode(input, final) + # If we haven't fixed the header yet, + # the content of ``self.buffer`` is a ``unicode`` object + output = self.buffer + self.decoder.decode(input, final) + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newoutput = _fixencoding(output, str(encoding), final) + if newoutput is None: + # retry fixing the @charset rule (but keep the decoded stuff) + self.buffer = output + return "" + self.headerfixed = True + return newoutput + + def reset(self): + codecs.IncrementalDecoder.reset(self) + self.decoder = None + self.buffer = b"" + self.headerfixed = False + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the real decoder too + if self.decoder is not None: + self.decoder.errors = errors + self._errors = errors + errors = property(_geterrors, _seterrors) + + def getstate(self): + if self.decoder is not None: + state = (self.encoding, self.buffer, self.headerfixed, True, self.decoder.getstate()) + else: + state = (self.encoding, self.buffer, self.headerfixed, False, None) + return ("", _bytes2int(marshal.dumps(state))) + + def setstate(self, state): + state = _int2bytes(marshal.loads(state[1])) # ignore buffered input + self.encoding = state[0] + self.buffer = state[1] + self.headerfixed = state[2] + if state[3] is not None: + self.decoder = codecs.getincrementaldecoder(self.encoding)(self._errors) + self.decoder.setstate(state[4]) + else: + self.decoder = None + + +if hasattr(codecs, "IncrementalEncoder"): + class IncrementalEncoder(codecs.IncrementalEncoder): + def __init__(self, errors="strict", encoding=None): + self.encoder = None + self.encoding = encoding + codecs.IncrementalEncoder.__init__(self, errors) + # Store ``errors`` somewhere else, + # because we have to hide it in a property + self._errors = errors + self.buffer = "" + + def iterencode(self, input): + for part in input: + result = self.encode(part, False) + if result: + yield result + result = self.encode("", True) + if result: + yield result + + def encode(self, input, final=False): + if self.encoder is None: + input = self.buffer + input + if self.encoding is not None: + # Replace encoding in the @charset rule with the specified one + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newinput = _fixencoding(input, str(encoding), final) + if newinput is None: # @charset rule incomplete => Retry next time + self.buffer = input + return "" + input = newinput + else: + # Use encoding from the @charset declaration + self.encoding = detectencoding_unicode(input, final)[0] + if self.encoding is not None: + if self.encoding == "css": + raise ValueError("css not allowed as encoding name") + info = codecs.lookup(self.encoding) + encoding = self.encoding + if self.encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, "utf-8", True) + self.encoder = info.incrementalencoder(self._errors) + self.buffer = "" + else: + self.buffer = input + return "" + return self.encoder.encode(input, final) + + def reset(self): + codecs.IncrementalEncoder.reset(self) + self.encoder = None + self.buffer = "" + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors ``must be done on the real encoder too + if self.encoder is not None: + self.encoder.errors = errors + self._errors = errors + errors = property(_geterrors, _seterrors) + + def getstate(self): + if self.encoder is not None: + state = (self.encoding, self.buffer, True, self.encoder.getstate()) + else: + state = (self.encoding, self.buffer, False, None) + return _bytes2int(marshal.dumps(state)) + + def setstate(self, state): + state = _int2bytes(marshal.loads(state)) + self.encoding = state[0] + self.buffer = state[1] + if state[2] is not None: + self.encoder = codecs.getincrementalencoder(self.encoding)(self._errors) + self.encoder.setstate(state[4]) + else: + self.encoder = None + + +class StreamWriter(codecs.StreamWriter): + def __init__(self, stream, errors="strict", encoding=None, header=False): + codecs.StreamWriter.__init__(self, stream, errors) + self.streamwriter = None + self.encoding = encoding + self._errors = errors + self.buffer = "" + + def encode(self, input, errors='strict'): + li = len(input) + if self.streamwriter is None: + input = self.buffer + input + li = len(input) + if self.encoding is not None: + # Replace encoding in the @charset rule with the specified one + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newinput = _fixencoding(input, str(encoding), False) + if newinput is None: # @charset rule incomplete => Retry next time + self.buffer = input + return ("", 0) + input = newinput + else: + # Use encoding from the @charset declaration + self.encoding = detectencoding_unicode(input, False)[0] + if self.encoding is not None: + if self.encoding == "css": + raise ValueError("css not allowed as encoding name") + self.streamwriter = codecs.getwriter(self.encoding)(self.stream, self._errors) + encoding = self.encoding + if self.encoding.replace("_", "-").lower() == "utf-8-sig": + input = _fixencoding(input, "utf-8", True) + self.buffer = "" + else: + self.buffer = input + return ("", 0) + return (self.streamwriter.encode(input, errors)[0], li) + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the streamwriter too + try: + if self.streamwriter is not None: + self.streamwriter.errors = errors + except AttributeError as e: + # TODO: py3 only exception? + pass + + self._errors = errors + errors = property(_geterrors, _seterrors) + + +class StreamReader(codecs.StreamReader): + def __init__(self, stream, errors="strict", encoding=None, force=True): + codecs.StreamReader.__init__(self, stream, errors) + self.streamreader = None + self.encoding = encoding + self.force = force + self._errors = errors + + def decode(self, input, errors='strict'): + if self.streamreader is None: + if self.encoding is None or not self.force: + (encoding, explicit) = detectencoding_str(input, False) + if encoding is None: # no encoding determined yet + return ("", 0) # no encoding determined yet, so no output + elif encoding == "css": + raise ValueError("css not allowed as encoding name") + if (explicit and not self.force) or self.encoding is None: # Take the encoding from the input + self.encoding = encoding + streamreader = codecs.getreader(self.encoding) + streamreader = streamreader(self.stream, self._errors) + (output, consumed) = streamreader.decode(input, errors) + encoding = self.encoding + if encoding.replace("_", "-").lower() == "utf-8-sig": + encoding = "utf-8" + newoutput = _fixencoding(output, str(encoding), False) + if newoutput is not None: + self.streamreader = streamreader + return (newoutput, consumed) + return ("", 0) # we will create a new streamreader on the next call + return self.streamreader.decode(input, errors) + + def _geterrors(self): + return self._errors + + def _seterrors(self, errors): + # Setting ``errors`` must be done on the streamreader too + try: + if self.streamreader is not None: + self.streamreader.errors = errors + except AttributeError as e: + # TODO: py3 only exception? + pass + + self._errors = errors + errors = property(_geterrors, _seterrors) + + +if hasattr(codecs, "CodecInfo"): + # We're running on Python 2.5 or better + def search_function(name): + if name == "css": + return codecs.CodecInfo( + name="css", + encode=encode, + decode=decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + ) +else: + # If we're running on Python 2.4, define the utf-8-sig codec here + def utf8sig_encode(input, errors='strict'): + return (codecs.BOM_UTF8 + codecs.utf_8_encode(input, errors)[0], len(input)) + + def utf8sig_decode(input, errors='strict'): + prefix = 0 + if input[:3] == codecs.BOM_UTF8: + input = input[3:] + prefix = 3 + (output, consumed) = codecs.utf_8_decode(input, errors, True) + return (output, consumed+prefix) + + class UTF8SigStreamWriter(codecs.StreamWriter): + def reset(self): + codecs.StreamWriter.reset(self) + try: + del self.encode + except AttributeError: + pass + + def encode(self, input, errors='strict'): + self.encode = codecs.utf_8_encode + return utf8sig_encode(input, errors) + + class UTF8SigStreamReader(codecs.StreamReader): + def reset(self): + codecs.StreamReader.reset(self) + try: + del self.decode + except AttributeError: + pass + + def decode(self, input, errors='strict'): + if len(input) < 3 and codecs.BOM_UTF8.startswith(input): + # not enough data to decide if this is a BOM + # => try again on the next call + return ("", 0) + self.decode = codecs.utf_8_decode + return utf8sig_decode(input, errors) + + def search_function(name): + import encodings + name = encodings.normalize_encoding(name) + if name == "css": + return (encode, decode, StreamReader, StreamWriter) + elif name == "utf_8_sig": + return (utf8sig_encode, utf8sig_decode, UTF8SigStreamReader, UTF8SigStreamWriter) + + +codecs.register(search_function) + + +# Error handler for CSS escaping + +def cssescape(exc): + if not isinstance(exc, UnicodeEncodeError): + raise TypeError("don't know how to handle %r" % exc) + return ("".join("\\%06x" % ord(c) for c in exc.object[exc.start:exc.end]), exc.end) + +codecs.register_error("cssescape", cssescape) diff --git a/libs/cssutils/_fetch.py b/libs/cssutils/_fetch.py new file mode 100755 index 00000000..a9138fe3 --- /dev/null +++ b/libs/cssutils/_fetch.py @@ -0,0 +1,44 @@ +"""Default URL reading functions""" +__all__ = ['_defaultFetcher'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: tokenize2.py 1547 2008-12-10 20:42:26Z cthedot $' + +import cssutils +from cssutils import VERSION +import encutils +import errorhandler +import urllib2 + +log = errorhandler.ErrorHandler() + +def _defaultFetcher(url): + """Retrieve data from ``url``. cssutils default implementation of fetch + URL function. + + Returns ``(encoding, string)`` or ``None`` + """ + try: + request = urllib2.Request(url) + request.add_header('User-agent', + 'cssutils %s (http://www.cthedot.de/cssutils/)' % VERSION) + res = urllib2.urlopen(request) + except OSError, e: + # e.g if file URL and not found + log.warn(e, error=OSError) + except (OSError, ValueError), e: + # invalid url, e.g. "1" + log.warn(u'ValueError, %s' % e.args[0], error=ValueError) + except urllib2.HTTPError, e: + # http error, e.g. 404, e can be raised + log.warn(u'HTTPError opening url=%s: %s %s' % + (url, e.code, e.msg), error=e) + except urllib2.URLError, e: + # URLError like mailto: or other IO errors, e can be raised + log.warn(u'URLError, %s' % e.reason, error=e) + else: + if res: + mimeType, encoding = encutils.getHTTPInfo(res) + if mimeType != u'text/css': + log.error(u'Expected "text/css" mime type for url=%r but found: %r' % + (url, mimeType), error=ValueError) + return encoding, res.read() diff --git a/libs/cssutils/_fetchgae.py b/libs/cssutils/_fetchgae.py new file mode 100755 index 00000000..ef91d412 --- /dev/null +++ b/libs/cssutils/_fetchgae.py @@ -0,0 +1,68 @@ +"""GAE specific URL reading functions""" +__all__ = ['_defaultFetcher'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: tokenize2.py 1547 2008-12-10 20:42:26Z cthedot $' + +# raises ImportError of not on GAE +from google.appengine.api import urlfetch +import cgi +import errorhandler +import util + +log = errorhandler.ErrorHandler() + +def _defaultFetcher(url): + """ + uses GoogleAppEngine (GAE) + fetch(url, payload=None, method=GET, headers={}, allow_truncated=False) + + Response + content + The body content of the response. + content_was_truncated + True if the allow_truncated parameter to fetch() was True and + the response exceeded the maximum response size. In this case, + the content attribute contains the truncated response. + status_code + The HTTP status code. + headers + The HTTP response headers, as a mapping of names to values. + + Exceptions + exception InvalidURLError() + The URL of the request was not a valid URL, or it used an + unsupported method. Only http and https URLs are supported. + exception DownloadError() + There was an error retrieving the data. + + This exception is not raised if the server returns an HTTP + error code: In that case, the response data comes back intact, + including the error code. + + exception ResponseTooLargeError() + The response data exceeded the maximum allowed size, and the + allow_truncated parameter passed to fetch() was False. + """ + #from google.appengine.api import urlfetch + try: + r = urlfetch.fetch(url, method=urlfetch.GET) + except urlfetch.Error, e: + log.warn(u'Error opening url=%r: %s' % (url, e), + error=IOError) + else: + if r.status_code == 200: + # find mimetype and encoding + mimetype = 'application/octet-stream' + try: + mimetype, params = cgi.parse_header(r.headers['content-type']) + encoding = params['charset'] + except KeyError: + encoding = None + if mimetype != u'text/css': + log.error(u'Expected "text/css" mime type for url %r but found: %r' % + (url, mimetype), error=ValueError) + return encoding, r.content + else: + # TODO: 301 etc + log.warn(u'Error opening url=%r: HTTP status %s' % + (url, r.status_code), error=IOError) diff --git a/libs/cssutils/codec.py b/libs/cssutils/codec.py new file mode 100755 index 00000000..c694e1fc --- /dev/null +++ b/libs/cssutils/codec.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +"""Python codec for CSS.""" +__docformat__ = 'restructuredtext' +__author__ = 'Walter Doerwald' +__version__ = '$Id: util.py 1114 2008-03-05 13:22:59Z cthedot $' + +import sys + +if sys.version_info < (3,): + from _codec2 import * + # for tests + from _codec2 import _fixencoding +else: + from _codec3 import * + # for tests + from _codec3 import _fixencoding diff --git a/libs/cssutils/css/__init__.py b/libs/cssutils/css/__init__.py new file mode 100755 index 00000000..4cc4a904 --- /dev/null +++ b/libs/cssutils/css/__init__.py @@ -0,0 +1,80 @@ +"""Implements Document Object Model Level 2 CSS +http://www.w3.org/TR/2000/PR-DOM-Level-2-Style-20000927/css.html + +currently implemented + - CSSStyleSheet + - CSSRuleList + - CSSRule + - CSSComment (cssutils addon) + - CSSCharsetRule + - CSSFontFaceRule + - CSSImportRule + - CSSMediaRule + - CSSNamespaceRule (WD) + - CSSPageRule + - CSSStyleRule + - CSSUnkownRule + - Selector and SelectorList + - CSSStyleDeclaration + - CSS2Properties + - CSSValue + - CSSPrimitiveValue + - CSSValueList + - CSSVariablesRule + - CSSVariablesDeclaration + +todo + - RGBColor, Rect, Counter +""" +__all__ = [ + 'CSSStyleSheet', + 'CSSRuleList', + 'CSSRule', + 'CSSComment', + 'CSSCharsetRule', + 'CSSFontFaceRule' + 'CSSImportRule', + 'CSSMediaRule', + 'CSSNamespaceRule', + 'CSSPageRule', + 'MarginRule', + 'CSSStyleRule', + 'CSSUnknownRule', + 'CSSVariablesRule', + 'CSSVariablesDeclaration', + 'Selector', 'SelectorList', + 'CSSStyleDeclaration', 'Property', + #'CSSValue', 'CSSPrimitiveValue', 'CSSValueList' + 'PropertyValue', + 'Value', + 'ColorValue', + 'DimensionValue', + 'URIValue', + 'CSSFunction', + 'CSSVariable', + 'MSValue' + ] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssstylesheet import * +from cssrulelist import * +from cssrule import * +from csscomment import * +from csscharsetrule import * +from cssfontfacerule import * +from cssimportrule import * +from cssmediarule import * +from cssnamespacerule import * +from csspagerule import * +from marginrule import * +from cssstylerule import * +from cssvariablesrule import * +from cssunknownrule import * +from selector import * +from selectorlist import * +from cssstyledeclaration import * +from cssvariablesdeclaration import * +from property import * +#from cssvalue import * +from value import * diff --git a/libs/cssutils/css/colors.py b/libs/cssutils/css/colors.py new file mode 100755 index 00000000..5f8dd899 --- /dev/null +++ b/libs/cssutils/css/colors.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +""" +Built from something like this: + + print [ + ( + row[2].text_content().strip(), + eval(row[4].text_content().strip()) + ) + for row in lxml.html.parse('http://www.w3.org/TR/css3-color/') + .xpath("//*[@class='colortable']//tr[position()>1]") + ] + +by Simon Sapin +""" + +COLORS = { + 'transparent': (0, 0, 0, 0.0), + + 'black': (0, 0, 0, 1.0), + 'silver': (192, 192, 192, 1.0), + 'gray': (128, 128, 128, 1.0), + 'white': (255, 255, 255, 1.0), + 'maroon': (128, 0, 0, 1.0), + 'red': (255, 0, 0, 1.0), + 'purple': (128, 0, 128, 1.0), + 'fuchsia': (255, 0, 255, 1.0), + 'green': (0, 128, 0, 1.0), + 'lime': (0, 255, 0, 1.0), + 'olive': (128, 128, 0, 1.0), + 'yellow': (255, 255, 0, 1.0), + 'navy': (0, 0, 128, 1.0), + 'blue': (0, 0, 255, 1.0), + 'teal': (0, 128, 128, 1.0), + + 'aqua': (0, 255, 255, 1.0), + 'aliceblue': (240, 248, 255, 1.0), + 'antiquewhite': (250, 235, 215, 1.0), + 'aqua': (0, 255, 255, 1.0), + 'aquamarine': (127, 255, 212, 1.0), + 'azure': (240, 255, 255, 1.0), + 'beige': (245, 245, 220, 1.0), + 'bisque': (255, 228, 196, 1.0), + 'black': (0, 0, 0, 1.0), + 'blanchedalmond': (255, 235, 205, 1.0), + 'blue': (0, 0, 255, 1.0), + 'blueviolet': (138, 43, 226, 1.0), + 'brown': (165, 42, 42, 1.0), + 'burlywood': (222, 184, 135, 1.0), + 'cadetblue': (95, 158, 160, 1.0), + 'chartreuse': (127, 255, 0, 1.0), + 'chocolate': (210, 105, 30, 1.0), + 'coral': (255, 127, 80, 1.0), + 'cornflowerblue': (100, 149, 237, 1.0), + 'cornsilk': (255, 248, 220, 1.0), + 'crimson': (220, 20, 60, 1.0), + 'cyan': (0, 255, 255, 1.0), + 'darkblue': (0, 0, 139, 1.0), + 'darkcyan': (0, 139, 139, 1.0), + 'darkgoldenrod': (184, 134, 11, 1.0), + 'darkgray': (169, 169, 169, 1.0), + 'darkgreen': (0, 100, 0, 1.0), + 'darkgrey': (169, 169, 169, 1.0), + 'darkkhaki': (189, 183, 107, 1.0), + 'darkmagenta': (139, 0, 139, 1.0), + 'darkolivegreen': (85, 107, 47, 1.0), + 'darkorange': (255, 140, 0, 1.0), + 'darkorchid': (153, 50, 204, 1.0), + 'darkred': (139, 0, 0, 1.0), + 'darksalmon': (233, 150, 122, 1.0), + 'darkseagreen': (143, 188, 143, 1.0), + 'darkslateblue': (72, 61, 139, 1.0), + 'darkslategray': (47, 79, 79, 1.0), + 'darkslategrey': (47, 79, 79, 1.0), + 'darkturquoise': (0, 206, 209, 1.0), + 'darkviolet': (148, 0, 211, 1.0), + 'deeppink': (255, 20, 147, 1.0), + 'deepskyblue': (0, 191, 255, 1.0), + 'dimgray': (105, 105, 105, 1.0), + 'dimgrey': (105, 105, 105, 1.0), + 'dodgerblue': (30, 144, 255, 1.0), + 'firebrick': (178, 34, 34, 1.0), + 'floralwhite': (255, 250, 240, 1.0), + 'forestgreen': (34, 139, 34, 1.0), + 'fuchsia': (255, 0, 255, 1.0), + 'gainsboro': (220, 220, 220, 1.0), + 'ghostwhite': (248, 248, 255, 1.0), + 'gold': (255, 215, 0, 1.0), + 'goldenrod': (218, 165, 32, 1.0), + 'gray': (128, 128, 128, 1.0), + 'green': (0, 128, 0, 1.0), + 'greenyellow': (173, 255, 47, 1.0), + 'grey': (128, 128, 128, 1.0), + 'honeydew': (240, 255, 240, 1.0), + 'hotpink': (255, 105, 180, 1.0), + 'indianred': (205, 92, 92, 1.0), + 'indigo': (75, 0, 130, 1.0), + 'ivory': (255, 255, 240, 1.0), + 'khaki': (240, 230, 140, 1.0), + 'lavender': (230, 230, 250, 1.0), + 'lavenderblush': (255, 240, 245, 1.0), + 'lawngreen': (124, 252, 0, 1.0), + 'lemonchiffon': (255, 250, 205, 1.0), + 'lightblue': (173, 216, 230, 1.0), + 'lightcoral': (240, 128, 128, 1.0), + 'lightcyan': (224, 255, 255, 1.0), + 'lightgoldenrodyellow': (250, 250, 210, 1.0), + 'lightgray': (211, 211, 211, 1.0), + 'lightgreen': (144, 238, 144, 1.0), + 'lightgrey': (211, 211, 211, 1.0), + 'lightpink': (255, 182, 193, 1.0), + 'lightsalmon': (255, 160, 122, 1.0), + 'lightseagreen': (32, 178, 170, 1.0), + 'lightskyblue': (135, 206, 250, 1.0), + 'lightslategray': (119, 136, 153, 1.0), + 'lightslategrey': (119, 136, 153, 1.0), + 'lightsteelblue': (176, 196, 222, 1.0), + 'lightyellow': (255, 255, 224, 1.0), + 'lime': (0, 255, 0, 1.0), + 'limegreen': (50, 205, 50, 1.0), + 'linen': (250, 240, 230, 1.0), + 'magenta': (255, 0, 255, 1.0), + 'maroon': (128, 0, 0, 1.0), + 'mediumaquamarine': (102, 205, 170, 1.0), + 'mediumblue': (0, 0, 205, 1.0), + 'mediumorchid': (186, 85, 211, 1.0), + 'mediumpurple': (147, 112, 219, 1.0), + 'mediumseagreen': (60, 179, 113, 1.0), + 'mediumslateblue': (123, 104, 238, 1.0), + 'mediumspringgreen': (0, 250, 154, 1.0), + 'mediumturquoise': (72, 209, 204, 1.0), + 'mediumvioletred': (199, 21, 133, 1.0), + 'midnightblue': (25, 25, 112, 1.0), + 'mintcream': (245, 255, 250, 1.0), + 'mistyrose': (255, 228, 225, 1.0), + 'moccasin': (255, 228, 181, 1.0), + 'navajowhite': (255, 222, 173, 1.0), + 'navy': (0, 0, 128, 1.0), + 'oldlace': (253, 245, 230, 1.0), + 'olive': (128, 128, 0, 1.0), + 'olivedrab': (107, 142, 35, 1.0), + 'orange': (255, 165, 0, 1.0), + 'orangered': (255, 69, 0, 1.0), + 'orchid': (218, 112, 214, 1.0), + 'palegoldenrod': (238, 232, 170, 1.0), + 'palegreen': (152, 251, 152, 1.0), + 'paleturquoise': (175, 238, 238, 1.0), + 'palevioletred': (219, 112, 147, 1.0), + 'papayawhip': (255, 239, 213, 1.0), + 'peachpuff': (255, 218, 185, 1.0), + 'peru': (205, 133, 63, 1.0), + 'pink': (255, 192, 203, 1.0), + 'plum': (221, 160, 221, 1.0), + 'powderblue': (176, 224, 230, 1.0), + 'purple': (128, 0, 128, 1.0), + 'red': (255, 0, 0, 1.0), + 'rosybrown': (188, 143, 143, 1.0), + 'royalblue': (65, 105, 225, 1.0), + 'saddlebrown': (139, 69, 19, 1.0), + 'salmon': (250, 128, 114, 1.0), + 'sandybrown': (244, 164, 96, 1.0), + 'seagreen': (46, 139, 87, 1.0), + 'seashell': (255, 245, 238, 1.0), + 'sienna': (160, 82, 45, 1.0), + 'silver': (192, 192, 192, 1.0), + 'skyblue': (135, 206, 235, 1.0), + 'slateblue': (106, 90, 205, 1.0), + 'slategray': (112, 128, 144, 1.0), + 'slategrey': (112, 128, 144, 1.0), + 'snow': (255, 250, 250, 1.0), + 'springgreen': (0, 255, 127, 1.0), + 'steelblue': (70, 130, 180, 1.0), + 'tan': (210, 180, 140, 1.0), + 'teal': (0, 128, 128, 1.0), + 'thistle': (216, 191, 216, 1.0), + 'tomato': (255, 99, 71, 1.0), + 'turquoise': (64, 224, 208, 1.0), + 'violet': (238, 130, 238, 1.0), + 'wheat': (245, 222, 179, 1.0), + 'white': (255, 255, 255, 1.0), + 'whitesmoke': (245, 245, 245, 1.0), + 'yellow': (255, 255, 0, 1.0), + 'yellowgreen': (154, 205, 50, 1.0), +} diff --git a/libs/cssutils/css/csscharsetrule.py b/libs/cssutils/css/csscharsetrule.py new file mode 100755 index 00000000..fa6b7c4d --- /dev/null +++ b/libs/cssutils/css/csscharsetrule.py @@ -0,0 +1,159 @@ +"""CSSCharsetRule implements DOM Level 2 CSS CSSCharsetRule.""" +__all__ = ['CSSCharsetRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import codecs +import cssrule +import cssutils +import xml.dom + +class CSSCharsetRule(cssrule.CSSRule): + """ + The CSSCharsetRule interface represents an @charset rule in a CSS style + sheet. The value of the encoding attribute does not affect the encoding + of text data in the DOM objects; this encoding is always UTF-16 + (also in Python?). After a stylesheet is loaded, the value of the + encoding attribute is the value found in the @charset rule. If there + was no @charset in the original document, then no CSSCharsetRule is + created. The value of the encoding attribute may also be used as a hint + for the encoding used on serialization of the style sheet. + + The value of the @charset rule (and therefore of the CSSCharsetRule) + may not correspond to the encoding the document actually came in; + character encoding information e.g. in an HTTP header, has priority + (see CSS document representation) but this is not reflected in the + CSSCharsetRule. + + This rule is not really needed anymore as setting + :attr:`CSSStyleSheet.encoding` is much easier. + + Format:: + + charsetrule: + CHARSET_SYM S* STRING S* ';' + + BUT: Only valid format is (single space, double quotes!):: + + @charset "ENCODING"; + """ + def __init__(self, encoding=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + :param encoding: + a valid character encoding + :param readonly: + defaults to False, not used yet + """ + super(CSSCharsetRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = '@charset' + + if encoding: + self.encoding = encoding + else: + self._encoding = None + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(encoding=%r)" % ( + self.__class__.__name__, + self.encoding) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.encoding, + id(self)) + + def _getCssText(self): + """The parsable textual representation.""" + return cssutils.ser.do_CSSCharsetRule(self) + + def _setCssText(self, cssText): + """ + :param cssText: + A parsable DOMString. + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSCharsetRule, self)._setCssText(cssText) + + wellformed = True + tokenizer = self._tokenize2(cssText) + + if self._type(self._nexttoken(tokenizer)) != self._prods.CHARSET_SYM: + wellformed = False + self._log.error(u'CSSCharsetRule must start with "@charset "', + error=xml.dom.InvalidModificationErr) + + encodingtoken = self._nexttoken(tokenizer) + encodingtype = self._type(encodingtoken) + encoding = self._stringtokenvalue(encodingtoken) + if self._prods.STRING != encodingtype or not encoding: + wellformed = False + self._log.error(u'CSSCharsetRule: no encoding found; %r.' % + self._valuestr(cssText)) + + semicolon = self._tokenvalue(self._nexttoken(tokenizer)) + EOFtype = self._type(self._nexttoken(tokenizer)) + if u';' != semicolon or EOFtype not in ('EOF', None): + wellformed = False + self._log.error(u'CSSCharsetRule: Syntax Error: %r.' % + self._valuestr(cssText)) + + if wellformed: + self.encoding = encoding + + cssText = property(fget=_getCssText, fset=_setCssText, + doc=u"(DOM) The parsable textual representation.") + + def _setEncoding(self, encoding): + """ + :param encoding: + a valid encoding to be used. Currently only valid Python encodings + are allowed. + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this encoding rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified encoding value has a syntax error and + is unparsable. + """ + self._checkReadonly() + tokenizer = self._tokenize2(encoding) + encodingtoken = self._nexttoken(tokenizer) + unexpected = self._nexttoken(tokenizer) + + if not encodingtoken or unexpected or\ + self._prods.IDENT != self._type(encodingtoken): + self._log.error(u'CSSCharsetRule: Syntax Error in encoding value ' + u'%r.' % encoding) + else: + try: + codecs.lookup(encoding) + except LookupError: + self._log.error(u'CSSCharsetRule: Unknown (Python) encoding %r.' + % encoding) + else: + self._encoding = encoding.lower() + + encoding = property(lambda self: self._encoding, _setEncoding, + doc=u"(DOM)The encoding information used in this @charset rule.") + + type = property(lambda self: self.CHARSET_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: bool(self.encoding)) diff --git a/libs/cssutils/css/csscomment.py b/libs/cssutils/css/csscomment.py new file mode 100755 index 00000000..311df0fd --- /dev/null +++ b/libs/cssutils/css/csscomment.py @@ -0,0 +1,87 @@ +"""CSSComment is not defined in DOM Level 2 at all but a cssutils defined +class only. + +Implements CSSRule which is also extended for a CSSComment rule type. +""" +__all__ = ['CSSComment'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import xml.dom + +class CSSComment(cssrule.CSSRule): + """ + Represents a CSS comment (cssutils only). + + Format:: + + /*...*/ + """ + def __init__(self, cssText=None, parentRule=None, + parentStyleSheet=None, readonly=False): + super(CSSComment, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + + self._cssText = None + if cssText: + self._setCssText(cssText) + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(cssText=%r)" % ( + self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.cssText, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSComment(self) + + def _setCssText(self, cssText): + """ + :param cssText: + textual text to set or tokenlist which is not tokenized + anymore. May also be a single token for this rule + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSComment, self)._setCssText(cssText) + tokenizer = self._tokenize2(cssText) + + commenttoken = self._nexttoken(tokenizer) + unexpected = self._nexttoken(tokenizer) + + if not commenttoken or\ + self._type(commenttoken) != self._prods.COMMENT or\ + unexpected: + self._log.error(u'CSSComment: Not a CSSComment: %r' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + self._cssText = self._tokenvalue(commenttoken) + + cssText = property(_getCssText, _setCssText, + doc=u"The parsable textual representation of this rule.") + + type = property(lambda self: self.COMMENT, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + # constant but needed: + wellformed = property(lambda self: True) diff --git a/libs/cssutils/css/cssfontfacerule.py b/libs/cssutils/css/cssfontfacerule.py new file mode 100755 index 00000000..1e936066 --- /dev/null +++ b/libs/cssutils/css/cssfontfacerule.py @@ -0,0 +1,184 @@ +"""CSSFontFaceRule implements DOM Level 2 CSS CSSFontFaceRule. + +From cssutils 0.9.6 additions from CSS Fonts Module Level 3 are +added http://www.w3.org/TR/css3-fonts/. +""" +__all__ = ['CSSFontFaceRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssstyledeclaration import CSSStyleDeclaration +import cssrule +import cssutils +import xml.dom + +class CSSFontFaceRule(cssrule.CSSRule): + """ + The CSSFontFaceRule interface represents a @font-face rule in a CSS + style sheet. The @font-face rule is used to hold a set of font + descriptions. + + Format:: + + font_face + : FONT_FACE_SYM S* + '{' S* declaration [ ';' S* declaration ]* '}' S* + ; + + cssutils uses a :class:`~cssutils.css.CSSStyleDeclaration` to + represent the font descriptions. For validation a specific profile + is used though were some properties have other valid values than + when used in e.g. a :class:`~cssutils.css.CSSStyleRule`. + """ + def __init__(self, style=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + If readonly allows setting of properties in constructor only. + + :param style: + CSSStyleDeclaration used to hold any font descriptions + for this CSSFontFaceRule + """ + super(CSSFontFaceRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@font-face' + + if style: + self.style = style + else: + self.style = CSSStyleDeclaration() + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(style=%r)" % ( + self.__class__.__name__, + self.style.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.style.cssText, + self.valid, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSFontFaceRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSFontFaceRule, self)._setCssText(cssText) + + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.FONT_FACE_SYM: + self._log.error(u'CSSFontFaceRule: No CSSFontFaceRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + newStyle = CSSStyleDeclaration(parentRule=self) + ok = True + + beforetokens, brace = self._tokensupto2(tokenizer, + blockstartonly=True, + separateEnd=True) + if self._tokenvalue(brace) != u'{': + ok = False + self._log.error(u'CSSFontFaceRule: No start { of style ' + u'declaration found: %r' + % self._valuestr(cssText), brace) + + # parse stuff before { which should be comments and S only + new = {'wellformed': True} + newseq = self._tempSeq() + + beforewellformed, expected = self._parse(expected=':', + seq=newseq, tokenizer=self._tokenize2(beforetokens), + productions={}) + ok = ok and beforewellformed and new['wellformed'] + + styletokens, braceorEOFtoken = self._tokensupto2(tokenizer, + blockendonly=True, + separateEnd=True) + + val, type_ = self._tokenvalue(braceorEOFtoken),\ + self._type(braceorEOFtoken) + if val != u'}' and type_ != 'EOF': + ok = False + self._log.error(u'CSSFontFaceRule: No "}" after style ' + u'declaration found: %r' + % self._valuestr(cssText)) + + nonetoken = self._nexttoken(tokenizer) + if nonetoken: + ok = False + self._log.error(u'CSSFontFaceRule: Trailing content found.', + token=nonetoken) + + if 'EOF' == type_: + # add again as style needs it + styletokens.append(braceorEOFtoken) + + # SET, may raise: + newStyle.cssText = styletokens + + if ok: + # contains probably comments only (upto ``{``) + self._setSeq(newseq) + self.style = newStyle + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + def _setStyle(self, style): + """ + :param style: + a CSSStyleDeclaration or string + """ + self._checkReadonly() + if isinstance(style, basestring): + self._style = CSSStyleDeclaration(cssText=style, parentRule=self) + else: + style._parentRule = self + self._style = style + + style = property(lambda self: self._style, _setStyle, + doc=u"(DOM) The declaration-block of this rule set, " + u"a :class:`~cssutils.css.CSSStyleDeclaration`.") + + type = property(lambda self: self.FONT_FACE_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + def _getValid(self): + needed = ['font-family', 'src'] + for p in self.style.getProperties(all=True): + if not p.valid: + return False + try: + needed.remove(p.name) + except ValueError: + pass + return not bool(needed) + + valid = property(_getValid, + doc=u"CSSFontFace is valid if properties `font-family` " + u"and `src` are set and all properties are valid.") + + # constant but needed: + wellformed = property(lambda self: True) diff --git a/libs/cssutils/css/cssimportrule.py b/libs/cssutils/css/cssimportrule.py new file mode 100755 index 00000000..59fe2564 --- /dev/null +++ b/libs/cssutils/css/cssimportrule.py @@ -0,0 +1,396 @@ +"""CSSImportRule implements DOM Level 2 CSS CSSImportRule plus the +``name`` property from http://www.w3.org/TR/css3-cascade/#cascading.""" +__all__ = ['CSSImportRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import os +import urlparse +import xml.dom + +class CSSImportRule(cssrule.CSSRule): + """ + Represents an @import rule within a CSS style sheet. The @import rule + is used to import style rules from other style sheets. + + Format:: + + import + : IMPORT_SYM S* + [STRING|URI] S* [ medium [ COMMA S* medium]* ]? S* STRING? S* ';' S* + ; + """ + def __init__(self, href=None, mediaText=None, name=None, + parentRule=None, parentStyleSheet=None, readonly=False): + """ + If readonly allows setting of properties in constructor only + + :param href: + location of the style sheet to be imported. + :param mediaText: + A list of media types for which this style sheet may be used + as a string + :param name: + Additional name of imported style sheet + """ + super(CSSImportRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@import' + self._styleSheet = None + + # string or uri used for reserialization + self.hreftype = None + + # prepare seq + seq = self._tempSeq() + seq.append(None, 'href') + #seq.append(None, 'media') + seq.append(None, 'name') + self._setSeq(seq) + + # 1. media + if mediaText: + self.media = mediaText + else: + # must be all for @import + self.media = cssutils.stylesheets.MediaList(mediaText=u'all') + # 2. name + self.name = name + # 3. href and styleSheet + self.href = href + + self._readonly = readonly + + def __repr__(self): + if self._usemedia: + mediaText = self.media.mediaText + else: + mediaText = None + return u"cssutils.css.%s(href=%r, mediaText=%r, name=%r)" % ( + self.__class__.__name__, + self.href, + self.media.mediaText, + self.name) + + def __str__(self): + if self._usemedia: + mediaText = self.media.mediaText + else: + mediaText = None + return u""\ + % (self.__class__.__name__, + self.href, + mediaText, + self.name, + id(self)) + + _usemedia = property(lambda self: self.media.mediaText not in (u'', u'all'), + doc="if self.media is used (or simply empty)") + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSImportRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + """ + super(CSSImportRule, self)._setCssText(cssText) + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.IMPORT_SYM: + self._log.error(u'CSSImportRule: No CSSImportRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + # for closures: must be a mutable + new = {'keyword': self._tokenvalue(attoken), + 'href': None, + 'hreftype': None, + 'media': None, + 'name': None, + 'wellformed': True + } + + def __doname(seq, token): + # called by _string or _ident + new['name'] = self._stringtokenvalue(token) + seq.append(new['name'], 'name') + return ';' + + def _string(expected, seq, token, tokenizer=None): + if 'href' == expected: + # href + new['href'] = self._stringtokenvalue(token) + new['hreftype'] = 'string' + seq.append(new['href'], 'href') + return 'media name ;' + elif 'name' in expected: + # name + return __doname(seq, token) + else: + new['wellformed'] = False + self._log.error( + u'CSSImportRule: Unexpected string.', token) + return expected + + def _uri(expected, seq, token, tokenizer=None): + # href + if 'href' == expected: + uri = self._uritokenvalue(token) + new['hreftype'] = 'uri' + new['href'] = uri + seq.append(new['href'], 'href') + return 'media name ;' + else: + new['wellformed'] = False + self._log.error( + u'CSSImportRule: Unexpected URI.', token) + return expected + + def _ident(expected, seq, token, tokenizer=None): + # medialist ending with ; which is checked upon too + if expected.startswith('media'): + mediatokens = self._tokensupto2( + tokenizer, importmediaqueryendonly=True) + mediatokens.insert(0, token) # push found token + + last = mediatokens.pop() # retrieve ; + lastval, lasttyp = self._tokenvalue(last), self._type(last) + if lastval != u';' and lasttyp not in ('EOF', + self._prods.STRING): + new['wellformed'] = False + self._log.error(u'CSSImportRule: No ";" found: %s' % + self._valuestr(cssText), token=token) + + newMedia = cssutils.stylesheets.MediaList(parentRule=self) + newMedia.mediaText = mediatokens + if newMedia.wellformed: + new['media'] = newMedia + seq.append(newMedia, 'media') + else: + new['wellformed'] = False + self._log.error(u'CSSImportRule: Invalid MediaList: %s' % + self._valuestr(cssText), token=token) + + if lasttyp == self._prods.STRING: + # name + return __doname(seq, last) + else: + return 'EOF' # ';' is token "last" + else: + new['wellformed'] = False + self._log.error(u'CSSImportRule: Unexpected ident.', token) + return expected + + def _char(expected, seq, token, tokenizer=None): + # final ; + val = self._tokenvalue(token) + if expected.endswith(';') and u';' == val: + return 'EOF' + else: + new['wellformed'] = False + self._log.error( + u'CSSImportRule: Unexpected char.', token) + return expected + + # import : IMPORT_SYM S* [STRING|URI] + # S* [ medium [ ',' S* medium]* ]? ';' S* + # STRING? # see http://www.w3.org/TR/css3-cascade/#cascading + # ; + newseq = self._tempSeq() + wellformed, expected = self._parse(expected='href', + seq=newseq, tokenizer=tokenizer, + productions={'STRING': _string, + 'URI': _uri, + 'IDENT': _ident, + 'CHAR': _char}, + new=new) + + # wellformed set by parse + ok = wellformed and new['wellformed'] + + # post conditions + if not new['href']: + ok = False + self._log.error(u'CSSImportRule: No href found: %s' % + self._valuestr(cssText)) + + if expected != 'EOF': + ok = False + self._log.error(u'CSSImportRule: No ";" found: %s' % + self._valuestr(cssText)) + + # set all + if ok: + self._setSeq(newseq) + + self.atkeyword = new['keyword'] + self.hreftype = new['hreftype'] + self.name = new['name'] + + if new['media']: + self.media = new['media'] + else: + # must be all for @import + self.media = cssutils.stylesheets.MediaList(mediaText=u'all') + + # needs new self.media + self.href = new['href'] + + cssText = property(fget=_getCssText, fset=_setCssText, + doc="(DOM) The parsable textual representation of this rule.") + + def _setHref(self, href): + # set new href + self._href = href + # update seq + for i, item in enumerate(self.seq): + val, type_ = item.value, item.type + if 'href' == type_: + self._seq[i] = (href, type_, item.line, item.col) + break + + importedSheet = cssutils.css.CSSStyleSheet(media=self.media, + ownerRule=self, + title=self.name) + self.hrefFound = False + # set styleSheet + if href and self.parentStyleSheet: + # loading errors are all catched! + + # relative href + parentHref = self.parentStyleSheet.href + if parentHref is None: + # use cwd instead + parentHref = cssutils.helper.path2url(os.getcwd()) + '/' + + fullhref = urlparse.urljoin(parentHref, self.href) + + # all possible exceptions are ignored + try: + usedEncoding, enctype, cssText = \ + self.parentStyleSheet._resolveImport(fullhref) + + if cssText is None: + # catched in next except below! + raise IOError('Cannot read Stylesheet.') + + # contentEncoding with parentStyleSheet.overrideEncoding, + # HTTP or parent + encodingOverride, encoding = None, None + + if enctype == 0: + encodingOverride = usedEncoding + elif 0 < enctype < 5: + encoding = usedEncoding + + # inherit fetcher for @imports in styleSheet + importedSheet._href = fullhref + importedSheet._setFetcher(self.parentStyleSheet._fetcher) + importedSheet._setCssTextWithEncodingOverride( + cssText, + encodingOverride=encodingOverride, + encoding=encoding) + + except (OSError, IOError, ValueError), e: + self._log.warn(u'CSSImportRule: While processing imported ' + u'style sheet href=%s: %r' + % (self.href, e), neverraise=True) + + else: + # used by resolveImports if to keep unprocessed href + self.hrefFound = True + + self._styleSheet = importedSheet + + _href = None # needs to be set + href = property(lambda self: self._href, _setHref, + doc=u"Location of the style sheet to be imported.") + + def _setMedia(self, media): + """ + :param media: + a :class:`~cssutils.stylesheets.MediaList` or string + """ + self._checkReadonly() + if isinstance(media, basestring): + self._media = cssutils.stylesheets.MediaList(mediaText=media, + parentRule=self) + else: + media._parentRule = self + self._media = media + + # update seq + ihref = 0 + for i, item in enumerate(self.seq): + if item.type == 'href': + ihref = i + elif item.type == 'media': + self.seq[i] = (self._media, 'media', None, None) + break + else: + # if no media until now add after href + self.seq.insert(ihref+1, + self._media, 'media', None, None) + + media = property(lambda self: self._media, _setMedia, + doc=u"(DOM) A list of media types for this rule " + u"of type :class:`~cssutils.stylesheets.MediaList`.") + + def _setName(self, name=u''): + """Raises xml.dom.SyntaxErr if name is not a string.""" + if name is None or isinstance(name, basestring): + # "" or '' handled as None + if not name: + name = None + + # save name + self._name = name + + # update seq + for i, item in enumerate(self.seq): + val, typ = item.value, item.type + if 'name' == typ: + self._seq[i] = (name, typ, item.line, item.col) + break + + # set title of imported sheet + if self.styleSheet: + self.styleSheet.title = name + + else: + self._log.error(u'CSSImportRule: Not a valid name: %s' % name) + + name = property(lambda self: self._name, _setName, + doc=u"An optional name for the imported sheet.") + + styleSheet = property(lambda self: self._styleSheet, + doc=u"(readonly) The style sheet referred to by this " + u"rule.") + + type = property(lambda self: self.IMPORT_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + def _getWellformed(self): + "Depending on if media is used at all." + if self._usemedia: + return bool(self.href and self.media.wellformed) + else: + return bool(self.href) + + wellformed = property(_getWellformed) diff --git a/libs/cssutils/css/cssmediarule.py b/libs/cssutils/css/cssmediarule.py new file mode 100755 index 00000000..a7ebf463 --- /dev/null +++ b/libs/cssutils/css/cssmediarule.py @@ -0,0 +1,302 @@ +"""CSSMediaRule implements DOM Level 2 CSS CSSMediaRule.""" +__all__ = ['CSSMediaRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import xml.dom + +class CSSMediaRule(cssrule.CSSRuleRules): + """ + Objects implementing the CSSMediaRule interface can be identified by the + MEDIA_RULE constant. On these objects the type attribute must return the + value of that constant. + + Format:: + + : MEDIA_SYM S* medium [ COMMA S* medium ]* + + STRING? # the name + + LBRACE S* ruleset* '}' S*; + + ``cssRules`` + All Rules in this media rule, a :class:`~cssutils.css.CSSRuleList`. + """ + def __init__(self, mediaText='all', name=None, + parentRule=None, parentStyleSheet=None, readonly=False): + """constructor""" + super(CSSMediaRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@media' + + # 1. media + if mediaText: + self.media = mediaText + else: + self.media = cssutils.stylesheets.MediaList() + + self.name = name + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(mediaText=%r)" % ( + self.__class__.__name__, + self.media.mediaText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.media.mediaText, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSMediaRule(self) + + def _setCssText(self, cssText): + """ + :param cssText: + a parseable string or a tuple of (cssText, dict-of-namespaces) + :Exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if a specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + # media "name"? { cssRules } + super(CSSMediaRule, self)._setCssText(cssText) + + # might be (cssText, namespaces) + cssText, namespaces = self._splitNamespacesOff(cssText) + + try: + # use parent style sheet ones if available + namespaces = self.parentStyleSheet.namespaces + except AttributeError: + pass + + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.MEDIA_SYM: + self._log.error(u'CSSMediaRule: No CSSMediaRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + + else: + # save if parse goes wrong + oldMedia = self._media + oldName = self._name + oldCssRules = self._cssRules + + ok = True + + # media + mediatokens, end = self._tokensupto2(tokenizer, + mediaqueryendonly=True, + separateEnd=True) + if u'{' == self._tokenvalue(end)\ + or self._prods.STRING == self._type(end): + self.media = cssutils.stylesheets.MediaList(parentRule=self) + # TODO: remove special case + self.media.mediaText = mediatokens + ok = ok and self.media.wellformed + else: + ok = False + + # name (optional) + name = None + nameseq = self._tempSeq() + if self._prods.STRING == self._type(end): + name = self._stringtokenvalue(end) + # TODO: for now comments are lost after name + nametokens, end = self._tokensupto2(tokenizer, + blockstartonly=True, + separateEnd=True) + wellformed, expected = self._parse(None, + nameseq, + nametokens, + {}) + if not wellformed: + ok = False + self._log.error(u'CSSMediaRule: Syntax Error: %s' % + self._valuestr(cssText)) + + + # check for { + if u'{' != self._tokenvalue(end): + self._log.error(u'CSSMediaRule: No "{" found: %s' % + self._valuestr(cssText)) + return + + # cssRules + cssrulestokens, braceOrEOF = self._tokensupto2(tokenizer, + mediaendonly=True, + separateEnd=True) + nonetoken = self._nexttoken(tokenizer, None) + if 'EOF' == self._type(braceOrEOF): + # HACK!!! + # TODO: Not complete, add EOF to rule and } to @media + cssrulestokens.append(braceOrEOF) + braceOrEOF = ('CHAR', '}', 0, 0) + self._log.debug(u'CSSMediaRule: Incomplete, adding "}".', + token=braceOrEOF, neverraise=True) + + if u'}' != self._tokenvalue(braceOrEOF): + self._log.error(u'CSSMediaRule: No "}" found.', + token=braceOrEOF) + elif nonetoken: + self._log.error(u'CSSMediaRule: Trailing content found.', + token=nonetoken) + else: + # for closures: must be a mutable + new = {'wellformed': True } + + def COMMENT(expected, seq, token, tokenizer=None): + self.insertRule(cssutils.css.CSSComment([token], + parentRule=self, + parentStyleSheet=self.parentStyleSheet)) + return expected + + def ruleset(expected, seq, token, tokenizer): + rule = cssutils.css.CSSStyleRule(parentRule=self, + parentStyleSheet=self.parentStyleSheet) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return expected + + def atrule(expected, seq, token, tokenizer): + # TODO: get complete rule! + tokens = self._tokensupto2(tokenizer, token) + atval = self._tokenvalue(token) + if atval in ('@charset ', '@font-face', '@import', + '@namespace', '@page', '@media', '@variables'): + self._log.error(u'CSSMediaRule: This rule is not ' + u'allowed in CSSMediaRule - ignored: ' + u'%s.' % self._valuestr(tokens), + token = token, + error=xml.dom.HierarchyRequestErr) + else: + rule = cssutils.css.CSSUnknownRule(tokens, + parentRule=self, + parentStyleSheet=self.parentStyleSheet) + if rule.wellformed: + self.insertRule(rule) + return expected + + # save for possible reset + oldCssRules = self.cssRules + + self.cssRules = cssutils.css.CSSRuleList() + seq = [] # not used really + + tokenizer = iter(cssrulestokens) + wellformed, expected = self._parse(braceOrEOF, + seq, + tokenizer, { + 'COMMENT': COMMENT, + 'CHARSET_SYM': atrule, + 'FONT_FACE_SYM': atrule, + 'IMPORT_SYM': atrule, + 'NAMESPACE_SYM': atrule, + 'PAGE_SYM': atrule, + 'MEDIA_SYM': atrule, + 'ATKEYWORD': atrule + }, + default=ruleset, + new=new) + ok = ok and wellformed + + if ok: + self.name = name + self._setSeq(nameseq) + else: + self._media = oldMedia + self._cssRules = oldCssRules + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + def _setName(self, name): + if isinstance(name, basestring) or name is None: + # "" or '' + if not name: + name = None + + self._name = name + else: + self._log.error(u'CSSImportRule: Not a valid name: %s' % name) + + name = property(lambda self: self._name, _setName, + doc=u"An optional name for this media rule.") + + def _setMedia(self, media): + """ + :param media: + a :class:`~cssutils.stylesheets.MediaList` or string + """ + self._checkReadonly() + if isinstance(media, basestring): + self._media = cssutils.stylesheets.MediaList(mediaText=media, + parentRule=self) + else: + media._parentRule = self + self._media = media + + # NOT IN @media seq at all?! +# # update seq +# for i, item in enumerate(self.seq): +# if item.type == 'media': +# self._seq[i] = (self._media, 'media', None, None) +# break +# else: +# # insert after @media if not in seq at all +# self.seq.insert(0, +# self._media, 'media', None, None) + + media = property(lambda self: self._media, _setMedia, + doc=u"(DOM) A list of media types for this rule " + u"of type :class:`~cssutils.stylesheets.MediaList`.") + + + def insertRule(self, rule, index=None): + """Implements base ``insertRule``.""" + rule, index = self._prepareInsertRule(rule, index) + + if rule is False or rule is True: + # done or error + return + + # check hierarchy + if isinstance(rule, cssutils.css.CSSCharsetRule) or \ + isinstance(rule, cssutils.css.CSSFontFaceRule) or \ + isinstance(rule, cssutils.css.CSSImportRule) or \ + isinstance(rule, cssutils.css.CSSNamespaceRule) or \ + isinstance(rule, cssutils.css.CSSPageRule) or \ + isinstance(rule, cssutils.css.MarginRule) or \ + isinstance(rule, CSSMediaRule): + self._log.error(u'%s: This type of rule is not allowed here: %s' + % (self.__class__.__name__, rule.cssText), + error=xml.dom.HierarchyRequestErr) + return + + return self._finishInsertRule(rule, index) + + type = property(lambda self: self.MEDIA_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: self.media.wellformed) diff --git a/libs/cssutils/css/cssnamespacerule.py b/libs/cssutils/css/cssnamespacerule.py new file mode 100755 index 00000000..a2610fc2 --- /dev/null +++ b/libs/cssutils/css/cssnamespacerule.py @@ -0,0 +1,295 @@ +"""CSSNamespaceRule currently implements http://dev.w3.org/csswg/css3-namespace/ +""" +__all__ = ['CSSNamespaceRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import xml.dom + +class CSSNamespaceRule(cssrule.CSSRule): + """ + Represents an @namespace rule within a CSS style sheet. + + The @namespace at-rule declares a namespace prefix and associates + it with a given namespace (a string). This namespace prefix can then be + used in namespace-qualified names such as those described in the + Selectors Module [SELECT] or the Values and Units module [CSS3VAL]. + + Dealing with these rules directly is not needed anymore, easier is + the use of :attr:`cssutils.css.CSSStyleSheet.namespaces`. + + Format:: + + namespace + : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S* + ; + namespace_prefix + : IDENT + ; + """ + def __init__(self, namespaceURI=None, prefix=None, cssText=None, + parentRule=None, parentStyleSheet=None, readonly=False): + """ + :Parameters: + namespaceURI + The namespace URI (a simple string!) which is bound to the + given prefix. If no prefix is set + (``CSSNamespaceRule.prefix==''``) the namespace defined by + namespaceURI is set as the default namespace + prefix + The prefix used in the stylesheet for the given + ``CSSNamespaceRule.uri``. + cssText + if no namespaceURI is given cssText must be given to set + a namespaceURI as this is readonly later on + parentStyleSheet + sheet where this rule belongs to + + Do not use as positional but as keyword parameters only! + + If readonly allows setting of properties in constructor only + + format namespace:: + + namespace + : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S* + ; + namespace_prefix + : IDENT + ; + """ + super(CSSNamespaceRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@namespace' + self._prefix = u'' + self._namespaceURI = None + + if namespaceURI: + self.namespaceURI = namespaceURI + self.prefix = prefix + tempseq = self._tempSeq() + tempseq.append(self.prefix, 'prefix') + tempseq.append(self.namespaceURI, 'namespaceURI') + self._setSeq(tempseq) + + elif cssText is not None: + self.cssText = cssText + + if parentStyleSheet: + self._parentStyleSheet = parentStyleSheet + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(namespaceURI=%r, prefix=%r)" % ( + self.__class__.__name__, + self.namespaceURI, + self.prefix) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.namespaceURI, + self.prefix, + id(self)) + + def _getCssText(self): + """Return serialized property cssText""" + return cssutils.ser.do_CSSNamespaceRule(self) + + def _setCssText(self, cssText): + """ + :param cssText: initial value for this rules cssText which is parsed + :exceptions: + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + """ + super(CSSNamespaceRule, self)._setCssText(cssText) + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.NAMESPACE_SYM: + self._log.error(u'CSSNamespaceRule: No CSSNamespaceRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + # for closures: must be a mutable + new = {'keyword': self._tokenvalue(attoken), + 'prefix': u'', + 'uri': None, + 'wellformed': True + } + + def _ident(expected, seq, token, tokenizer=None): + # the namespace prefix, optional + if 'prefix or uri' == expected: + new['prefix'] = self._tokenvalue(token) + seq.append(new['prefix'], 'prefix') + return 'uri' + else: + new['wellformed'] = False + self._log.error( + u'CSSNamespaceRule: Unexpected ident.', token) + return expected + + def _string(expected, seq, token, tokenizer=None): + # the namespace URI as a STRING + if expected.endswith('uri'): + new['uri'] = self._stringtokenvalue(token) + seq.append(new['uri'], 'namespaceURI') + return ';' + + else: + new['wellformed'] = False + self._log.error( + u'CSSNamespaceRule: Unexpected string.', token) + return expected + + def _uri(expected, seq, token, tokenizer=None): + # the namespace URI as URI which is DEPRECATED + if expected.endswith('uri'): + uri = self._uritokenvalue(token) + new['uri'] = uri + seq.append(new['uri'], 'namespaceURI') + return ';' + else: + new['wellformed'] = False + self._log.error( + u'CSSNamespaceRule: Unexpected URI.', token) + return expected + + def _char(expected, seq, token, tokenizer=None): + # final ; + val = self._tokenvalue(token) + if ';' == expected and u';' == val: + return 'EOF' + else: + new['wellformed'] = False + self._log.error( + u'CSSNamespaceRule: Unexpected char.', token) + return expected + + # "NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*" + newseq = self._tempSeq() + wellformed, expected = self._parse(expected='prefix or uri', + seq=newseq, tokenizer=tokenizer, + productions={'IDENT': _ident, + 'STRING': _string, + 'URI': _uri, + 'CHAR': _char}, + new=new) + + # wellformed set by parse + wellformed = wellformed and new['wellformed'] + + # post conditions + if new['uri'] is None: + wellformed = False + self._log.error(u'CSSNamespaceRule: No namespace URI found: %s' + % self._valuestr(cssText)) + + if expected != 'EOF': + wellformed = False + self._log.error(u'CSSNamespaceRule: No ";" found: %s' % + self._valuestr(cssText)) + + # set all + if wellformed: + self.atkeyword = new['keyword'] + self._prefix = new['prefix'] + self.namespaceURI = new['uri'] + self._setSeq(newseq) + + cssText = property(fget=_getCssText, fset=_setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + def _setNamespaceURI(self, namespaceURI): + """ + :param namespaceURI: the initial value for this rules namespaceURI + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + (CSSRule) Raised if this rule is readonly or a namespaceURI is + already set in this rule. + """ + self._checkReadonly() + if not self._namespaceURI: + # initial setting + self._namespaceURI = namespaceURI + tempseq = self._tempSeq() + tempseq.append(namespaceURI, 'namespaceURI') + self._setSeq(tempseq) # makes seq readonly! + elif self._namespaceURI != namespaceURI: + self._log.error(u'CSSNamespaceRule: namespaceURI is readonly.', + error=xml.dom.NoModificationAllowedErr) + + namespaceURI = property(lambda self: self._namespaceURI, _setNamespaceURI, + doc="URI (handled as simple string) of the defined namespace.") + + def _replaceNamespaceURI(self, namespaceURI): + """Used during parse of new sheet only! + + :param namespaceURI: the new value for this rules namespaceURI + """ + self._namespaceURI = namespaceURI + for i, x in enumerate(self._seq): + if 'namespaceURI' == x.type: + self._seq._readonly = False + self._seq.replace(i, namespaceURI, 'namespaceURI') + self._seq._readonly = True + break + + def _setPrefix(self, prefix=None): + """ + :param prefix: the new prefix + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + if not prefix: + prefix = u'' + else: + tokenizer = self._tokenize2(prefix) + prefixtoken = self._nexttoken(tokenizer, None) + if not prefixtoken or self._type(prefixtoken) != self._prods.IDENT: + self._log.error(u'CSSNamespaceRule: No valid prefix "%s".' % + self._valuestr(prefix), + error=xml.dom.SyntaxErr) + return + else: + prefix = self._tokenvalue(prefixtoken) + # update seq + for i, x in enumerate(self._seq): + if x == self._prefix: + self._seq[i] = (prefix, 'prefix', None, None) + break + else: + # put prefix at the beginning! + self._seq[0] = (prefix, 'prefix', None, None) + + # set new prefix + self._prefix = prefix + + prefix = property(lambda self: self._prefix, _setPrefix, + doc=u"Prefix used for the defined namespace.") + + type = property(lambda self: self.NAMESPACE_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: self.namespaceURI is not None) + \ No newline at end of file diff --git a/libs/cssutils/css/csspagerule.py b/libs/cssutils/css/csspagerule.py new file mode 100755 index 00000000..cc2f4516 --- /dev/null +++ b/libs/cssutils/css/csspagerule.py @@ -0,0 +1,436 @@ +"""CSSPageRule implements DOM Level 2 CSS CSSPageRule.""" +__all__ = ['CSSPageRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from itertools import chain +from cssstyledeclaration import CSSStyleDeclaration +from marginrule import MarginRule +import cssrule +import cssutils +import xml.dom + +class CSSPageRule(cssrule.CSSRuleRules): + """ + The CSSPageRule interface represents a @page rule within a CSS style + sheet. The @page rule is used to specify the dimensions, orientation, + margins, etc. of a page box for paged media. + + Format:: + + page : + PAGE_SYM S* IDENT? pseudo_page? S* + '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* + ; + + pseudo_page : + ':' [ "left" | "right" | "first" ] + ; + + margin : + margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* + ; + + margin_sym : + TOPLEFTCORNER_SYM | + TOPLEFT_SYM | + TOPCENTER_SYM | + TOPRIGHT_SYM | + TOPRIGHTCORNER_SYM | + BOTTOMLEFTCORNER_SYM | + BOTTOMLEFT_SYM | + BOTTOMCENTER_SYM | + BOTTOMRIGHT_SYM | + BOTTOMRIGHTCORNER_SYM | + LEFTTOP_SYM | + LEFTMIDDLE_SYM | + LEFTBOTTOM_SYM | + RIGHTTOP_SYM | + RIGHTMIDDLE_SYM | + RIGHTBOTTOM_SYM + ; + + `cssRules` contains a list of `MarginRule` objects. + """ + def __init__(self, selectorText=None, style=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + If readonly allows setting of properties in constructor only. + + :param selectorText: + type string + :param style: + CSSStyleDeclaration for this CSSStyleRule + """ + super(CSSPageRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@page' + self._specificity = (0, 0, 0) + + tempseq = self._tempSeq() + + if selectorText: + self.selectorText = selectorText + tempseq.append(self.selectorText, 'selectorText') + else: + self._selectorText = self._tempSeq() + + if style: + self.style = style + else: + self.style = CSSStyleDeclaration() + + tempseq.append(self.style, 'style') + + self._setSeq(tempseq) + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(selectorText=%r, style=%r)" % ( + self.__class__.__name__, + self.selectorText, + self.style.cssText) + + def __str__(self): + return (u"") % ( + self.__class__.__name__, + self.selectorText, + self.specificity, + self.style.cssText, + len(self.cssRules), + id(self)) + + def __contains__(self, margin): + """Check if margin is set in the rule.""" + return margin in self.keys() + + def keys(self): + "Return list of all set margins (MarginRule)." + return list(r.margin for r in self.cssRules) + + def __getitem__(self, margin): + """Retrieve the style (of MarginRule) + for `margin` (which must be normalized). + """ + for r in self.cssRules: + if r.margin == margin: + return r.style + + def __setitem__(self, margin, style): + """Set the style (of MarginRule) + for `margin` (which must be normalized). + """ + for i, r in enumerate(self.cssRules): + if r.margin == margin: + r.style = style + return i + else: + return self.add(MarginRule(margin, style)) + + def __delitem__(self, margin): + """Delete the style (the MarginRule) + for `margin` (which must be normalized). + """ + for r in self.cssRules: + if r.margin == margin: + self.deleteRule(r) + + def __parseSelectorText(self, selectorText): + """ + Parse `selectorText` which may also be a list of tokens + and returns (selectorText, seq). + + see _setSelectorText for details + """ + # for closures: must be a mutable + new = {'wellformed': True, 'last-S': False, + 'name': 0, 'first': 0, 'lr': 0} + specificity = (0, 0, 0) + + def _char(expected, seq, token, tokenizer=None): + # pseudo_page, :left, :right or :first + val = self._tokenvalue(token) + if not new['last-S'] and expected in ['page', ': or EOF']\ + and u':' == val: + try: + identtoken = tokenizer.next() + except StopIteration: + self._log.error( + u'CSSPageRule selectorText: No IDENT found.', token) + else: + ival, ityp = self._tokenvalue(identtoken),\ + self._type(identtoken) + if self._prods.IDENT != ityp: + self._log.error(u'CSSPageRule selectorText: Expected ' + u'IDENT but found: %r' % ival, token) + else: + if not ival in (u'first', u'left', u'right'): + self._log.warn(u'CSSPageRule: Unknown @page ' + u'selector: %r' + % (u':'+ival,), neverraise=True) + if ival == u'first': + new['first'] = 1 + else: + new['lr'] = 1 + seq.append(val + ival, 'pseudo') + return 'EOF' + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSPageRule selectorText: Unexpected CHAR: %r' + % val, token) + return expected + + def S(expected, seq, token, tokenizer=None): + "Does not raise if EOF is found." + if expected == ': or EOF': + # pseudo must directly follow IDENT if given + new['last-S'] = True + return expected + + def IDENT(expected, seq, token, tokenizer=None): + "" + val = self._tokenvalue(token) + if 'page' == expected: + if self._normalize(val) == u'auto': + self._log.error(u'CSSPageRule selectorText: Invalid pagename.', + token) + else: + new['name'] = 1 + seq.append(val, 'IDENT') + + return ': or EOF' + else: + new['wellformed'] = False + self._log.error(u'CSSPageRule selectorText: Unexpected IDENT: ' + u'%r' % val, token) + return expected + + def COMMENT(expected, seq, token, tokenizer=None): + "Does not raise if EOF is found." + seq.append(cssutils.css.CSSComment([token]), 'COMMENT') + return expected + + newseq = self._tempSeq() + wellformed, expected = self._parse(expected='page', + seq=newseq, tokenizer=self._tokenize2(selectorText), + productions={'CHAR': _char, + 'IDENT': IDENT, + 'COMMENT': COMMENT, + 'S': S}, + new=new) + wellformed = wellformed and new['wellformed'] + + # post conditions + if expected == 'ident': + self._log.error( + u'CSSPageRule selectorText: No valid selector: %r' % + self._valuestr(selectorText)) + + return wellformed, newseq, (new['name'], new['first'], new['lr']) + + + def __parseMarginAndStyle(self, tokens): + "tokens is a list, no generator (yet)" + g = iter(tokens) + styletokens = [] + + # new rules until parse done + cssRules = [] + + for token in g: + if token[0] == 'ATKEYWORD' and \ + self._normalize(token[1]) in MarginRule.margins: + + # MarginRule + m = MarginRule(parentRule=self, + parentStyleSheet=self.parentStyleSheet) + m.cssText = chain([token], g) + + # merge if margin set more than once + for r in cssRules: + if r.margin == m.margin: + for p in m.style: + r.style.setProperty(p, replace=False) + break + else: + cssRules.append(m) + + continue + + # TODO: Properties? + styletokens.append(token) + + return cssRules, styletokens + + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSPageRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSPageRule, self)._setCssText(cssText) + + tokenizer = self._tokenize2(cssText) + if self._type(self._nexttoken(tokenizer)) != self._prods.PAGE_SYM: + self._log.error(u'CSSPageRule: No CSSPageRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + newStyle = CSSStyleDeclaration(parentRule=self) + ok = True + + selectortokens, startbrace = self._tokensupto2(tokenizer, + blockstartonly=True, + separateEnd=True) + styletokens, braceorEOFtoken = self._tokensupto2(tokenizer, + blockendonly=True, + separateEnd=True) + nonetoken = self._nexttoken(tokenizer) + if self._tokenvalue(startbrace) != u'{': + ok = False + self._log.error(u'CSSPageRule: No start { of style declaration ' + u'found: %r' % + self._valuestr(cssText), startbrace) + elif nonetoken: + ok = False + self._log.error(u'CSSPageRule: Trailing content found.', + token=nonetoken) + + selok, newselseq, specificity = self.__parseSelectorText(selectortokens) + ok = ok and selok + + val, type_ = self._tokenvalue(braceorEOFtoken),\ + self._type(braceorEOFtoken) + + if val != u'}' and type_ != 'EOF': + ok = False + self._log.error( + u'CSSPageRule: No "}" after style declaration found: %r' % + self._valuestr(cssText)) + else: + if 'EOF' == type_: + # add again as style needs it + styletokens.append(braceorEOFtoken) + + # filter pagemargin rules out first + cssRules, styletokens = self.__parseMarginAndStyle(styletokens) + + # SET, may raise: + newStyle.cssText = styletokens + + if ok: + self._selectorText = newselseq + self._specificity = specificity + self.style = newStyle + self.cssRules = cssutils.css.CSSRuleList() + for r in cssRules: + self.cssRules.append(r) + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this rule.") + + + def _getSelectorText(self): + """Wrapper for cssutils Selector object.""" + return cssutils.ser.do_CSSPageRuleSelector(self._selectorText) + + def _setSelectorText(self, selectorText): + """Wrapper for cssutils Selector object. + + :param selectorText: + DOM String, in CSS 2.1 one of + + - :first + - :left + - :right + - empty + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + # may raise SYNTAX_ERR + wellformed, newseq, specificity = self.__parseSelectorText(selectorText) + if wellformed: + self._selectorText = newseq + self._specificity = specificity + + selectorText = property(_getSelectorText, _setSelectorText, + doc=u"(DOM) The parsable textual representation of " + u"the page selector for the rule.") + + def _setStyle(self, style): + """ + :param style: + a CSSStyleDeclaration or string + """ + self._checkReadonly() + if isinstance(style, basestring): + self._style = CSSStyleDeclaration(cssText=style, parentRule=self) + else: + style._parentRule = self + self._style = style + + style = property(lambda self: self._style, _setStyle, + doc=u"(DOM) The declaration-block of this rule set, " + u"a :class:`~cssutils.css.CSSStyleDeclaration`.") + + + def insertRule(self, rule, index=None): + """Implements base ``insertRule``.""" + rule, index = self._prepareInsertRule(rule, index) + + if rule is False or rule is True: + # done or error + return + + # check hierarchy + if isinstance(rule, cssutils.css.CSSCharsetRule) or \ + isinstance(rule, cssutils.css.CSSFontFaceRule) or \ + isinstance(rule, cssutils.css.CSSImportRule) or \ + isinstance(rule, cssutils.css.CSSNamespaceRule) or \ + isinstance(rule, CSSPageRule) or \ + isinstance(rule, cssutils.css.CSSMediaRule): + self._log.error(u'%s: This type of rule is not allowed here: %s' + % (self.__class__.__name__, rule.cssText), + error=xml.dom.HierarchyRequestErr) + return + + return self._finishInsertRule(rule, index) + + specificity = property(lambda self: self._specificity, + doc=u"""Specificity of this page rule (READONLY). +Tuple of (f, g, h) where: + + - if the page selector has a named page, f=1; else f=0 + - if the page selector has a ':first' pseudo-class, g=1; else g=0 + - if the page selector has a ':left' or ':right' pseudo-class, h=1; else h=0 +""") + + type = property(lambda self: self.PAGE_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + # constant but needed: + wellformed = property(lambda self: True) \ No newline at end of file diff --git a/libs/cssutils/css/cssproperties.py b/libs/cssutils/css/cssproperties.py new file mode 100755 index 00000000..7454daff --- /dev/null +++ b/libs/cssutils/css/cssproperties.py @@ -0,0 +1,122 @@ +"""CSS2Properties (partly!) implements DOM Level 2 CSS CSS2Properties used +by CSSStyleDeclaration + +TODO: CSS2Properties + If an implementation does implement this interface, it is expected to + understand the specific syntax of the shorthand properties, and apply + their semantics; when the margin property is set, for example, the + marginTop, marginRight, marginBottom and marginLeft properties are + actually being set by the underlying implementation. + + When dealing with CSS "shorthand" properties, the shorthand properties + should be decomposed into their component longhand properties as + appropriate, and when querying for their value, the form returned + should be the shortest form exactly equivalent to the declarations made + in the ruleset. However, if there is no shorthand declaration that + could be added to the ruleset without changing in any way the rules + already declared in the ruleset (i.e., by adding longhand rules that + were previously not declared in the ruleset), then the empty string + should be returned for the shorthand property. + + For example, querying for the font property should not return + "normal normal normal 14pt/normal Arial, sans-serif", when + "14pt Arial, sans-serif" suffices. (The normals are initial values, and + are implied by use of the longhand property.) + + If the values for all the longhand properties that compose a particular + string are the initial values, then a string consisting of all the + initial values should be returned (e.g. a border-width value of + "medium" should be returned as such, not as ""). + + For some shorthand properties that take missing values from other + sides, such as the margin, padding, and border-[width|style|color] + properties, the minimum number of sides possible should be used; i.e., + "0px 10px" will be returned instead of "0px 10px 0px 10px". + + If the value of a shorthand property can not be decomposed into its + component longhand properties, as is the case for the font property + with a value of "menu", querying for the values of the component + longhand properties should return the empty string. + +TODO: CSS2Properties DOMImplementation + The interface found within this section are not mandatory. A DOM + application can use the hasFeature method of the DOMImplementation + interface to determine whether it is supported or not. The feature + string for this extended interface listed in this section is "CSS2" + and the version is "2.0". + +""" +__all__ = ['CSS2Properties'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssutils.profiles +import re + +class CSS2Properties(object): + """The CSS2Properties interface represents a convenience mechanism + for retrieving and setting properties within a CSSStyleDeclaration. + The attributes of this interface correspond to all the properties + specified in CSS2. Getting an attribute of this interface is + equivalent to calling the getPropertyValue method of the + CSSStyleDeclaration interface. Setting an attribute of this + interface is equivalent to calling the setProperty method of the + CSSStyleDeclaration interface. + + cssutils actually also allows usage of ``del`` to remove a CSS property + from a CSSStyleDeclaration. + + This is an abstract class, the following functions need to be present + in inheriting class: + + - ``_getP`` + - ``_setP`` + - ``_delP`` + """ + # actual properties are set after the class definition! + def _getP(self, CSSname): pass + def _setP(self, CSSname, value): pass + def _delP(self, CSSname): pass + + +_reCSStoDOMname = re.compile('-[a-z]', re.I) +def _toDOMname(CSSname): + """Returns DOMname for given CSSname e.g. for CSSname 'font-style' returns + 'fontStyle'. + """ + def _doCSStoDOMname2(m): return m.group(0)[1].capitalize() + return _reCSStoDOMname.sub(_doCSStoDOMname2, CSSname) + +_reDOMtoCSSname = re.compile('([A-Z])[a-z]+') +def _toCSSname(DOMname): + """Return CSSname for given DOMname e.g. for DOMname 'fontStyle' returns + 'font-style'. + """ + def _doDOMtoCSSname2(m): return '-' + m.group(0).lower() + return _reDOMtoCSSname.sub(_doDOMtoCSSname2, DOMname) + +# add list of DOMname properties to CSS2Properties +# used for CSSStyleDeclaration to check if allowed properties +# but somehow doubled, any better way? +CSS2Properties._properties = [] +for group in cssutils.profiles.properties: + for name in cssutils.profiles.properties[group]: + CSS2Properties._properties.append(_toDOMname(name)) + + +# add CSS2Properties to CSSStyleDeclaration: +def __named_property_def(DOMname): + """ + Closure to keep name known in each properties accessor function + DOMname is converted to CSSname here, so actual calls use CSSname. + """ + CSSname = _toCSSname(DOMname) + def _get(self): return self._getP(CSSname) + def _set(self, value): self._setP(CSSname, value) + def _del(self): self._delP(CSSname) + return _get, _set, _del + +# add all CSS2Properties to CSSStyleDeclaration +for DOMname in CSS2Properties._properties: + setattr(CSS2Properties, DOMname, + property(*__named_property_def(DOMname))) diff --git a/libs/cssutils/css/cssrule.py b/libs/cssutils/css/cssrule.py new file mode 100755 index 00000000..1d3ab6fb --- /dev/null +++ b/libs/cssutils/css/cssrule.py @@ -0,0 +1,304 @@ +"""CSSRule implements DOM Level 2 CSS CSSRule.""" +__all__ = ['CSSRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssutils +import xml.dom + +class CSSRule(cssutils.util.Base2): + """Abstract base interface for any type of CSS statement. This includes + both rule sets and at-rules. An implementation is expected to preserve + all rules specified in a CSS style sheet, even if the rule is not + recognized by the parser. Unrecognized rules are represented using the + :class:`CSSUnknownRule` interface. + """ + + """ + CSSRule type constants. + An integer indicating which type of rule this is. + """ + UNKNOWN_RULE = 0 + ":class:`cssutils.css.CSSUnknownRule` (not used in CSSOM anymore)" + STYLE_RULE = 1 + ":class:`cssutils.css.CSSStyleRule`" + CHARSET_RULE = 2 + ":class:`cssutils.css.CSSCharsetRule` (not used in CSSOM anymore)" + IMPORT_RULE = 3 + ":class:`cssutils.css.CSSImportRule`" + MEDIA_RULE = 4 + ":class:`cssutils.css.CSSMediaRule`" + FONT_FACE_RULE = 5 + ":class:`cssutils.css.CSSFontFaceRule`" + PAGE_RULE = 6 + ":class:`cssutils.css.CSSPageRule`" + NAMESPACE_RULE = 10 + """:class:`cssutils.css.CSSNamespaceRule`, + Value has changed in 0.9.7a3 due to a change in the CSSOM spec.""" + COMMENT = 1001 # was -1, cssutils only + """:class:`cssutils.css.CSSComment` - not in the offical spec, + Value has changed in 0.9.7a3""" + VARIABLES_RULE = 1008 + """:class:`cssutils.css.CSSVariablesRule` - experimental rule + not in the offical spec""" + + MARGIN_RULE = 1006 + """:class:`cssutils.css.MarginRule` - experimental rule + not in the offical spec""" + + _typestrings = {UNKNOWN_RULE: u'UNKNOWN_RULE', + STYLE_RULE: u'STYLE_RULE', + CHARSET_RULE: u'CHARSET_RULE', + IMPORT_RULE: u'IMPORT_RULE', + MEDIA_RULE: u'MEDIA_RULE', + FONT_FACE_RULE: u'FONT_FACE_RULE', + PAGE_RULE: u'PAGE_RULE', + NAMESPACE_RULE: u'NAMESPACE_RULE', + COMMENT: u'COMMENT', + VARIABLES_RULE: u'VARIABLES_RULE', + MARGIN_RULE: u'MARGIN_RULE' + } + + def __init__(self, parentRule=None, parentStyleSheet=None, readonly=False): + """Set common attributes for all rules.""" + super(CSSRule, self).__init__() + self._parent = parentRule + self._parentRule = parentRule + self._parentStyleSheet = parentStyleSheet + self._setSeq(self._tempSeq()) + #self._atkeyword = None + # must be set after initialization of #inheriting rule is done + self._readonly = False + + def _setAtkeyword(self, keyword): + """Check if new keyword fits the rule it is used for.""" + atkeyword = self._normalize(keyword) + if not self.atkeyword or (self.atkeyword == atkeyword): + self._atkeyword = atkeyword + self._keyword = keyword + else: + self._log.error(u'%s: Invalid atkeyword for this rule: %r' % + (self.atkeyword, keyword), + error=xml.dom.InvalidModificationErr) + + atkeyword = property(lambda self: self._atkeyword, _setAtkeyword, + doc=u"Normalized keyword of an @rule (e.g. ``@import``).") + + def _setCssText(self, cssText): + """ + :param cssText: + A parsable DOMString. + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + self._checkReadonly() + + cssText = property(lambda self: u'', _setCssText, + doc=u"(DOM) The parsable textual representation of the " + u"rule. This reflects the current state of the rule " + u"and not its initial value.") + + parent = property(lambda self: self._parent, + doc=u"The Parent Node of this CSSRule or None.") + + parentRule = property(lambda self: self._parentRule, + doc=u"If this rule is contained inside another rule " + u"(e.g. a style rule inside an @media block), this " + u"is the containing rule. If this rule is not nested " + u"inside any other rules, this returns None.") + + def _getParentStyleSheet(self): + # rules contained in other rules (@media) use that rules parent + if (self.parentRule): + return self.parentRule._parentStyleSheet + else: + return self._parentStyleSheet + + parentStyleSheet = property(_getParentStyleSheet, + doc=u"The style sheet that contains this rule.") + + type = property(lambda self: self.UNKNOWN_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + typeString = property(lambda self: CSSRule._typestrings[self.type], + doc=u"Descriptive name of this rule's type.") + + wellformed = property(lambda self: False, + doc=u"If the rule is wellformed.") + + + +class CSSRuleRules(CSSRule): + """Abstract base interface for rules that contain other rules + like @media or @page. Methods may be overwritten if a rule + has specific stuff to do like checking the order of insertion like + @media does. + """ + + def __init__(self, parentRule=None, parentStyleSheet=None): + + super(CSSRuleRules, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + + self.cssRules = cssutils.css.CSSRuleList() + + def __iter__(self): + """Generator iterating over these rule's cssRules.""" + for rule in self._cssRules: + yield rule + + def _setCssRules(self, cssRules): + "Set new cssRules and update contained rules refs." + cssRules.append = self.insertRule + cssRules.extend = self.insertRule + cssRules.__delitem__ == self.deleteRule + + for rule in cssRules: + rule._parentRule = self + rule._parentStyleSheet = None + + self._cssRules = cssRules + + cssRules = property(lambda self: self._cssRules, _setCssRules, + "All Rules in this style sheet, a " + ":class:`~cssutils.css.CSSRuleList`.") + + def deleteRule(self, index): + """ + Delete the rule at `index` from rules ``cssRules``. + + :param index: + The `index` of the rule to be removed from the rules cssRules + list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is + raised but rules for normal Python lists are used. E.g. + ``deleteRule(-1)`` removes the last rule in cssRules. + + `index` may also be a CSSRule object which will then be removed. + + :Exceptions: + - :exc:`~xml.dom.IndexSizeErr`: + Raised if the specified index does not correspond to a rule in + the media rule list. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this media rule is readonly. + """ + self._checkReadonly() + + if isinstance(index, CSSRule): + for i, r in enumerate(self.cssRules): + if index == r: + index = i + break + else: + raise xml.dom.IndexSizeErr(u"%s: Not a rule in " + u"this rule'a cssRules list: %s" + % (self.__class__.__name__, index)) + + try: + # detach + self._cssRules[index]._parentRule = None + del self._cssRules[index] + + except IndexError: + raise xml.dom.IndexSizeErr(u'%s: %s is not a valid index ' + u'in the rulelist of length %i' + % (self.__class__.__name__, + index, self._cssRules.length)) + + def _prepareInsertRule(self, rule, index=None): + "return checked `index` and optional parsed `rule`" + self._checkReadonly() + + # check index + if index is None: + index = len(self._cssRules) + + elif index < 0 or index > self._cssRules.length: + raise xml.dom.IndexSizeErr(u'%s: Invalid index %s for ' + u'CSSRuleList with a length of %s.' + % (self.__class__.__name__, + index, self._cssRules.length)) + + # check and optionally parse rule + if isinstance(rule, basestring): + tempsheet = cssutils.css.CSSStyleSheet() + tempsheet.cssText = rule + if len(tempsheet.cssRules) != 1 or (tempsheet.cssRules and + not isinstance(tempsheet.cssRules[0], cssutils.css.CSSRule)): + self._log.error(u'%s: Invalid Rule: %s' % (self.__class__.__name__, + rule)) + return False, False + rule = tempsheet.cssRules[0] + + elif isinstance(rule, cssutils.css.CSSRuleList): + # insert all rules + for i, r in enumerate(rule): + self.insertRule(r, index + i) + return True, True + + elif not isinstance(rule, cssutils.css.CSSRule): + self._log.error(u'%s: Not a CSSRule: %s' % (rule, + self.__class__.__name__)) + return False, False + + return rule, index + + def _finishInsertRule(self, rule, index): + "add `rule` at `index`" + rule._parentRule = self + rule._parentStyleSheet = None + self._cssRules.insert(index, rule) + return index + + def add(self, rule): + """Add `rule` to page rule. Same as ``insertRule(rule)``.""" + return self.insertRule(rule) + + def insertRule(self, rule, index=None): + """ + Insert `rule` into the rules ``cssRules``. + + :param rule: + the parsable text representing the `rule` to be inserted. For rule + sets this contains both the selector and the style declaration. + For at-rules, this specifies both the at-identifier and the rule + content. + + cssutils also allows rule to be a valid + :class:`~cssutils.css.CSSRule` object. + + :param index: + before the `index` the specified `rule` will be inserted. + If the specified `index` is equal to the length of the rules + rule collection, the rule will be added to the end of the rule. + If index is not given or None rule will be appended to rule + list. + + :returns: + the index of the newly inserted rule. + + :exceptions: + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the `rule` cannot be inserted at the specified `index`, + e.g., if an @import rule is inserted after a standard rule set + or other at-rule. + - :exc:`~xml.dom.IndexSizeErr`: + Raised if the specified `index` is not a valid insertion point. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified `rule` has a syntax error and is + unparsable. + """ + return self._prepareInsertRule(rule, index) diff --git a/libs/cssutils/css/cssrulelist.py b/libs/cssutils/css/cssrulelist.py new file mode 100755 index 00000000..f9daff49 --- /dev/null +++ b/libs/cssutils/css/cssrulelist.py @@ -0,0 +1,53 @@ +"""CSSRuleList implements DOM Level 2 CSS CSSRuleList. +Partly also http://dev.w3.org/csswg/cssom/#the-cssrulelist.""" +__all__ = ['CSSRuleList'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +class CSSRuleList(list): + """The CSSRuleList object represents an (ordered) list of statements. + + The items in the CSSRuleList are accessible via an integral index, + starting from 0. + + Subclasses a standard Python list so theoretically all standard list + methods are available. Setting methods like ``__init__``, ``append``, + ``extend`` or ``__setslice__`` are added later on instances of this + class if so desired. + E.g. CSSStyleSheet adds ``append`` which is not available in a simple + instance of this class! + """ + def __init__(self, *ignored): + "Nothing is set as this must also be defined later." + pass + + def __notimplemented(self, *ignored): + "Implemented in class using a CSSRuleList only." + raise NotImplementedError( + 'Must be implemented by class using an instance of this class.') + + append = extend = __setitem__ = __setslice__ = __notimplemented + + def item(self, index): + """(DOM) Retrieve a CSS rule by ordinal `index`. The order in this + collection represents the order of the rules in the CSS style + sheet. If index is greater than or equal to the number of rules in + the list, this returns None. + + Returns CSSRule, the style rule at the index position in the + CSSRuleList, or None if that is not a valid index. + """ + try: + return self[index] + except IndexError: + return None + + length = property(lambda self: len(self), + doc=u"(DOM) The number of CSSRules in the list.") + + def rulesOfType(self, type): + """Yield the rules which have the given `type` only, one of the + constants defined in :class:`cssutils.css.CSSRule`.""" + for r in self: + if r.type == type: + yield r diff --git a/libs/cssutils/css/cssstyledeclaration.py b/libs/cssutils/css/cssstyledeclaration.py new file mode 100755 index 00000000..7573487a --- /dev/null +++ b/libs/cssutils/css/cssstyledeclaration.py @@ -0,0 +1,697 @@ +"""CSSStyleDeclaration implements DOM Level 2 CSS CSSStyleDeclaration and +extends CSS2Properties + +see + http://www.w3.org/TR/1998/REC-CSS2-19980512/syndata.html#parsing-errors + +Unknown properties +------------------ +User agents must ignore a declaration with an unknown property. +For example, if the style sheet is:: + + H1 { color: red; rotation: 70minutes } + +the user agent will treat this as if the style sheet had been:: + + H1 { color: red } + +Cssutils gives a message about any unknown properties but +keeps any property (if syntactically correct). + +Illegal values +-------------- +User agents must ignore a declaration with an illegal value. For example:: + + IMG { float: left } /* correct CSS2 */ + IMG { float: left here } /* "here" is not a value of 'float' */ + IMG { background: "red" } /* keywords cannot be quoted in CSS2 */ + IMG { border-width: 3 } /* a unit must be specified for length values */ + +A CSS2 parser would honor the first rule and ignore the rest, as if the +style sheet had been:: + + IMG { float: left } + IMG { } + IMG { } + IMG { } + +Cssutils again will issue a message (WARNING in this case) about invalid +CSS2 property values. + +TODO: + This interface is also used to provide a read-only access to the + computed values of an element. See also the ViewCSS interface. + + - return computed values and not literal values + - simplify unit pairs/triples/quadruples + 2px 2px 2px 2px -> 2px for border/padding... + - normalize compound properties like: + background: no-repeat left url() #fff + -> background: #fff url() no-repeat left +""" +__all__ = ['CSSStyleDeclaration', 'Property'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssproperties import CSS2Properties +from property import Property +import cssutils +import xml.dom + +class CSSStyleDeclaration(CSS2Properties, cssutils.util.Base2): + """The CSSStyleDeclaration class represents a single CSS declaration + block. This class may be used to determine the style properties + currently set in a block or to set style properties explicitly + within the block. + + While an implementation may not recognize all CSS properties within + a CSS declaration block, it is expected to provide access to all + specified properties in the style sheet through the + CSSStyleDeclaration interface. + Furthermore, implementations that support a specific level of CSS + should correctly handle CSS shorthand properties for that level. For + a further discussion of shorthand properties, see the CSS2Properties + interface. + + Additionally the CSS2Properties interface is implemented. + + $css2propertyname + All properties defined in the CSS2Properties class are available + as direct properties of CSSStyleDeclaration with their respective + DOM name, so e.g. ``fontStyle`` for property 'font-style'. + + These may be used as:: + + >>> style = CSSStyleDeclaration(cssText='color: red') + >>> style.color = 'green' + >>> print style.color + green + >>> del style.color + >>> print style.color + + + Format:: + + [Property: Value Priority?;]* [Property: Value Priority?]? + """ + def __init__(self, cssText=u'', parentRule=None, readonly=False, + validating=None): + """ + :param cssText: + Shortcut, sets CSSStyleDeclaration.cssText + :param parentRule: + The CSS rule that contains this declaration block or + None if this CSSStyleDeclaration is not attached to a CSSRule. + :param readonly: + defaults to False + :param validating: + a flag defining if this sheet should be validated on change. + Defaults to None, which means defer to the parent stylesheet. + """ + super(CSSStyleDeclaration, self).__init__() + self._parentRule = parentRule + self.validating = validating + self.cssText = cssText + self._readonly = readonly + + def __contains__(self, nameOrProperty): + """Check if a property (or a property with given name) is in style. + + :param name: + a string or Property, uses normalized name and not literalname + """ + if isinstance(nameOrProperty, Property): + name = nameOrProperty.name + else: + name = self._normalize(nameOrProperty) + return name in self.__nnames() + + def __iter__(self): + """Iterator of set Property objects with different normalized names.""" + def properties(): + for name in self.__nnames(): + yield self.getProperty(name) + return properties() + + def keys(self): + """Analoguous to standard dict returns property names which are set in + this declaration.""" + return list(self.__nnames()) + + def __getitem__(self, CSSName): + """Retrieve the value of property ``CSSName`` from this declaration. + + ``CSSName`` will be always normalized. + """ + return self.getPropertyValue(CSSName) + + def __setitem__(self, CSSName, value): + """Set value of property ``CSSName``. ``value`` may also be a tuple of + (value, priority), e.g. style['color'] = ('red', 'important') + + ``CSSName`` will be always normalized. + """ + priority = None + if isinstance(value, tuple): + value, priority = value + + return self.setProperty(CSSName, value, priority) + + def __delitem__(self, CSSName): + """Delete property ``CSSName`` from this declaration. + If property is not in this declaration return u'' just like + removeProperty. + + ``CSSName`` will be always normalized. + """ + return self.removeProperty(CSSName) + + def __setattr__(self, n, v): + """Prevent setting of unknown properties on CSSStyleDeclaration + which would not work anyway. For these + ``CSSStyleDeclaration.setProperty`` MUST be called explicitly! + + TODO: + implementation of known is not really nice, any alternative? + """ + known = ['_tokenizer', '_log', '_ttypes', + '_seq', 'seq', 'parentRule', '_parentRule', 'cssText', + 'valid', 'wellformed', 'validating', + '_readonly', '_profiles', '_validating'] + known.extend(CSS2Properties._properties) + if n in known: + super(CSSStyleDeclaration, self).__setattr__(n, v) + else: + raise AttributeError(u'Unknown CSS Property, ' + u'``CSSStyleDeclaration.setProperty("%s", ' + u'...)`` MUST be used.' % n) + + def __repr__(self): + return u"cssutils.css.%s(cssText=%r)" % ( + self.__class__.__name__, + self.getCssText(separator=u' ')) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.length, + len(self.getProperties(all=True)), + id(self)) + + def __nnames(self): + """Return iterator for all different names in order as set + if names are set twice the last one is used (double reverse!) + """ + names = [] + for item in reversed(self.seq): + val = item.value + if isinstance(val, Property) and not val.name in names: + names.append(val.name) + return reversed(names) + + # overwritten accessor functions for CSS2Properties' properties + def _getP(self, CSSName): + """(DOM CSS2Properties) Overwritten here and effectively the same as + ``self.getPropertyValue(CSSname)``. + + Parameter is in CSSname format ('font-style'), see CSS2Properties. + + Example:: + + >>> style = CSSStyleDeclaration(cssText='font-style:italic;') + >>> print style.fontStyle + italic + """ + return self.getPropertyValue(CSSName) + + def _setP(self, CSSName, value): + """(DOM CSS2Properties) Overwritten here and effectively the same as + ``self.setProperty(CSSname, value)``. + + Only known CSS2Properties may be set this way, otherwise an + AttributeError is raised. + For these unknown properties ``setPropertyValue(CSSname, value)`` + has to be called explicitly. + Also setting the priority of properties needs to be done with a + call like ``setPropertyValue(CSSname, value, priority)``. + + Example:: + + >>> style = CSSStyleDeclaration() + >>> style.fontStyle = 'italic' + >>> # or + >>> style.setProperty('font-style', 'italic', '!important') + + """ + self.setProperty(CSSName, value) + # TODO: Shorthand ones + + def _delP(self, CSSName): + """(cssutils only) Overwritten here and effectively the same as + ``self.removeProperty(CSSname)``. + + Example:: + + >>> style = CSSStyleDeclaration(cssText='font-style:italic;') + >>> del style.fontStyle + >>> print style.fontStyle + + + """ + self.removeProperty(CSSName) + + def children(self): + """Generator yielding any known child in this declaration including + *all* properties, comments or CSSUnknownrules. + """ + for item in self._seq: + yield item.value + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_css_CSSStyleDeclaration(self) + + def _setCssText(self, cssText): + """Setting this attribute will result in the parsing of the new value + and resetting of all the properties in the declaration block + including the removal or addition of properties. + + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or a property is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + """ + self._checkReadonly() + tokenizer = self._tokenize2(cssText) + + # for closures: must be a mutable + new = {'wellformed': True} + def ident(expected, seq, token, tokenizer=None): + # a property + + tokens = self._tokensupto2(tokenizer, starttoken=token, + semicolon=True) + if self._tokenvalue(tokens[-1]) == u';': + tokens.pop() + property = Property(parent=self) + property.cssText = tokens + if property.wellformed: + seq.append(property, 'Property') + else: + self._log.error(u'CSSStyleDeclaration: Syntax Error in ' + u'Property: %s' % self._valuestr(tokens)) + # does not matter in this case + return expected + + def unexpected(expected, seq, token, tokenizer=None): + # error, find next ; or } to omit upto next property + ignored = self._tokenvalue(token) + self._valuestr( + self._tokensupto2(tokenizer, + propertyvalueendonly=True)) + self._log.error(u'CSSStyleDeclaration: Unexpected token, ignoring ' + 'upto %r.' % ignored,token) + # does not matter in this case + return expected + + def char(expected, seq, token, tokenizer=None): + # a standalone ; or error... + if self._tokenvalue(token) == u';': + self._log.info(u'CSSStyleDeclaration: Stripped standalone semicolon' + u': %s' % self._valuestr([token]), neverraise=True) + return expected + else: + return unexpected(expected, seq, token, tokenizer) + + # [Property: Value;]* Property: Value? + newseq = self._tempSeq() + wellformed, expected = self._parse(expected=None, + seq=newseq, tokenizer=tokenizer, + productions={'IDENT': ident, 'CHAR': char}, + default=unexpected) + # wellformed set by parse + + for item in newseq: + item.value._parent = self + + # do not check wellformed as invalid things are removed anyway + self._setSeq(newseq) + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) A parsable textual representation of the " + u"declaration block excluding the surrounding curly " + u"braces.") + + def getCssText(self, separator=None): + """ + :returns: + serialized property cssText, each property separated by + given `separator` which may e.g. be ``u''`` to be able to use + cssText directly in an HTML style attribute. ``;`` is part of + each property (except the last one) and **cannot** be set with + separator! + """ + return cssutils.ser.do_css_CSSStyleDeclaration(self, separator) + + def _setParentRule(self, parentRule): + self._parentRule = parentRule +# for x in self.children(): +# x.parent = self + + parentRule = property(lambda self: self._parentRule, _setParentRule, + doc="(DOM) The CSS rule that contains this declaration block or " + "None if this CSSStyleDeclaration is not attached to a CSSRule.") + + def getProperties(self, name=None, all=False): + """ + :param name: + optional `name` of properties which are requested. + Only properties with this **always normalized** `name` are returned. + If `name` is ``None`` all properties are returned (at least one for + each set name depending on parameter `all`). + :param all: + if ``False`` (DEFAULT) only the effective properties are returned. + If name is given a list with only one property is returned. + + if ``True`` all properties including properties set multiple times + with different values or priorities for different UAs are returned. + The order of the properties is fully kept as in the original + stylesheet. + :returns: + a list of :class:`~cssutils.css.Property` objects set in + this declaration. + """ + if name and not all: + # single prop but list + p = self.getProperty(name) + if p: + return [p] + else: + return [] + elif not all: + # effective Properties in name order + return [self.getProperty(name) for name in self.__nnames()] + else: + # all properties or all with this name + nname = self._normalize(name) + properties = [] + for item in self.seq: + val = item.value + if isinstance(val, Property) and ( + (bool(nname) == False) or (val.name == nname)): + properties.append(val) + return properties + + def getProperty(self, name, normalize=True): + """ + :param name: + of the CSS property, always lowercase (even if not normalized) + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized name. + :returns: + the effective :class:`~cssutils.css.Property` object. + """ + nname = self._normalize(name) + found = None + for item in reversed(self.seq): + val = item.value + if isinstance(val, Property): + if (normalize and nname == val.name) or name == val.literalname: + if val.priority: + return val + elif not found: + found = val + return found + + def getPropertyCSSValue(self, name, normalize=True): + """ + :param name: + of the CSS property, always lowercase (even if not normalized) + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized name. + :returns: + :class:`~cssutils.css.CSSValue`, the value of the effective + property if it has been explicitly set for this declaration block. + + (DOM) + Used to retrieve the object representation of the value of a CSS + property if it has been explicitly set within this declaration + block. Returns None if the property has not been set. + + (This method returns None if the property is a shorthand + property. Shorthand property values can only be accessed and + modified as strings, using the getPropertyValue and setProperty + methods.) + + **cssutils currently always returns a CSSValue if the property is + set.** + + for more on shorthand properties see + http://www.dustindiaz.com/css-shorthand/ + """ + nname = self._normalize(name) + if nname in self._SHORTHANDPROPERTIES: + self._log.info(u'CSSValue for shorthand property "%s" should be ' + u'None, this may be implemented later.' % + nname, neverraise=True) + + p = self.getProperty(name, normalize) + if p: + return p.cssValue + else: + return None + + def getPropertyValue(self, name, normalize=True): + """ + :param name: + of the CSS property, always lowercase (even if not normalized) + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized name. + :returns: + the value of the effective property if it has been explicitly set + for this declaration block. Returns the empty string if the + property has not been set. + """ + p = self.getProperty(name, normalize) + if p: + return p.value + else: + return u'' + + def getPropertyPriority(self, name, normalize=True): + """ + :param name: + of the CSS property, always lowercase (even if not normalized) + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized name. + :returns: + the priority of the effective CSS property (e.g. the + "important" qualifier) if the property has been explicitly set in + this declaration block. The empty string if none exists. + """ + p = self.getProperty(name, normalize) + if p: + return p.priority + else: + return u'' + + def removeProperty(self, name, normalize=True): + """ + (DOM) + Used to remove a CSS property if it has been explicitly set within + this declaration block. + + :param name: + of the CSS property + :param normalize: + if ``True`` (DEFAULT) name will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent. + The effective Property value is returned and *all* Properties + with ``Property.name == name`` are removed. + + If ``False`` may return **NOT** the effective value but the + effective for the unnormalized `name` only. Also only the + Properties with the literal name `name` are removed. + :returns: + the value of the property if it has been explicitly set for + this declaration block. Returns the empty string if the property + has not been set or the property name does not correspond to a + known CSS property + + + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or the property is + readonly. + """ + self._checkReadonly() + r = self.getPropertyValue(name, normalize=normalize) + newseq = self._tempSeq() + if normalize: + # remove all properties with name == nname + nname = self._normalize(name) + for item in self.seq: + if not (isinstance(item.value, Property) + and item.value.name == nname): + newseq.appendItem(item) + else: + # remove all properties with literalname == name + for item in self.seq: + if not (isinstance(item.value, Property) + and item.value.literalname == name): + newseq.appendItem(item) + self._setSeq(newseq) + return r + + def setProperty(self, name, value=None, priority=u'', + normalize=True, replace=True): + """(DOM) Set a property value and priority within this declaration + block. + + :param name: + of the CSS property to set (in W3C DOM the parameter is called + "propertyName"), always lowercase (even if not normalized) + + If a property with this `name` is present it will be reset. + + cssutils also allowed `name` to be a + :class:`~cssutils.css.Property` object, all other + parameter are ignored in this case + + :param value: + the new value of the property, ignored if `name` is a Property. + :param priority: + the optional priority of the property (e.g. "important"), + ignored if `name` is a Property. + :param normalize: + if True (DEFAULT) `name` will be normalized (lowercase, no simple + escapes) so "color", "COLOR" or "C\olor" will all be equivalent + :param replace: + if True (DEFAULT) the given property will replace a present + property. If False a new property will be added always. + The difference to `normalize` is that two or more properties with + the same name may be set, useful for e.g. stuff like:: + + background: red; + background: rgba(255, 0, 0, 0.5); + + which defines the same property but only capable UAs use the last + property value, older ones use the first value. + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified value has a syntax error and is + unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or the property is + readonly. + """ + self._checkReadonly() + + if isinstance(name, Property): + newp = name + name = newp.literalname + elif not value: + # empty string or None effectively removed property + return self.removeProperty(name) + else: + newp = Property(name, value, priority) + + if newp.wellformed: + if replace: + # check if update + nname = self._normalize(name) + properties = self.getProperties(name, all=(not normalize)) + for property in reversed(properties): + if normalize and property.name == nname: + property.cssValue = newp.cssValue.cssText + property.priority = newp.priority + return + elif property.literalname == name: + property.cssValue = newp.cssValue.cssText + property.priority = newp.priority + return + + # not yet set or forced omit replace + newp.parent = self + self.seq._readonly = False + self.seq.append(newp, 'Property') + self.seq._readonly = True + + else: + self._log.warn(u'Invalid Property: %s: %s %s' + % (name, value, priority)) + + def item(self, index): + """(DOM) Retrieve the properties that have been explicitly set in + this declaration block. The order of the properties retrieved using + this method does not have to be the order in which they were set. + This method can be used to iterate over all properties in this + declaration block. + + :param index: + of the property to retrieve, negative values behave like + negative indexes on Python lists, so -1 is the last element + + :returns: + the name of the property at this ordinal position. The + empty string if no property exists at this position. + + **ATTENTION:** + Only properties with different names are counted. If two + properties with the same name are present in this declaration + only the effective one is included. + + :meth:`item` and :attr:`length` work on the same set here. + """ + names = list(self.__nnames()) + try: + return names[index] + except IndexError: + return u'' + + length = property(lambda self: len(list(self.__nnames())), + doc=u"(DOM) The number of distinct properties that have " + u"been explicitly in this declaration block. The " + u"range of valid indices is 0 to length-1 inclusive. " + u"These are properties with a different ``name`` " + u"only. :meth:`item` and :attr:`length` work on the " + u"same set here.") + + def _getValidating(self): + try: + # CSSParser.parseX() sets validating of stylesheet + return self.parentRule.parentStyleSheet.validating + except AttributeError: + # CSSParser.parseStyle() sets validating of declaration + if self._validating is not None: + return self._validating + # default + return True + + def _setValidating(self, validating): + self._validating = validating + + validating = property(_getValidating, _setValidating, + doc=u"If ``True`` this declaration validates " + u"contained properties. The parent StyleSheet " + u"validation setting does *always* win though so " + u"even if validating is True it may not validate " + u"if the StyleSheet defines else!") diff --git a/libs/cssutils/css/cssstylerule.py b/libs/cssutils/css/cssstylerule.py new file mode 100755 index 00000000..db85b8b5 --- /dev/null +++ b/libs/cssutils/css/cssstylerule.py @@ -0,0 +1,234 @@ +"""CSSStyleRule implements DOM Level 2 CSS CSSStyleRule.""" +__all__ = ['CSSStyleRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssstyledeclaration import CSSStyleDeclaration +from selectorlist import SelectorList +import cssrule +import cssutils +import xml.dom + +class CSSStyleRule(cssrule.CSSRule): + """The CSSStyleRule object represents a ruleset specified (if any) in a CSS + style sheet. It provides access to a declaration block as well as to the + associated group of selectors. + + Format:: + + : selector [ COMMA S* selector ]* + LBRACE S* declaration [ ';' S* declaration ]* '}' S* + ; + """ + def __init__(self, selectorText=None, style=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + :Parameters: + selectorText + string parsed into selectorList + style + string parsed into CSSStyleDeclaration for this CSSStyleRule + readonly + if True allows setting of properties in constructor only + """ + super(CSSStyleRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + + self.selectorList = SelectorList() + if selectorText: + self.selectorText = selectorText + + if style: + self.style = style + else: + self.style = CSSStyleDeclaration() + + self._readonly = readonly + + def __repr__(self): + if self._namespaces: + st = (self.selectorText, self._namespaces) + else: + st = self.selectorText + return u"cssutils.css.%s(selectorText=%r, style=%r)" % ( + self.__class__.__name__, st, self.style.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.selectorText, + self.style.cssText, + self._namespaces, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSStyleRule(self) + + def _setCssText(self, cssText): + """ + :param cssText: + a parseable string or a tuple of (cssText, dict-of-namespaces) + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSStyleRule, self)._setCssText(cssText) + + # might be (cssText, namespaces) + cssText, namespaces = self._splitNamespacesOff(cssText) + try: + # use parent style sheet ones if available + namespaces = self.parentStyleSheet.namespaces + except AttributeError: + pass + + tokenizer = self._tokenize2(cssText) + selectortokens = self._tokensupto2(tokenizer, blockstartonly=True) + styletokens = self._tokensupto2(tokenizer, blockendonly=True) + trail = self._nexttoken(tokenizer) + if trail: + self._log.error(u'CSSStyleRule: Trailing content: %s' % + self._valuestr(cssText), token=trail) + elif not selectortokens: + self._log.error(u'CSSStyleRule: No selector found: %r' % + self._valuestr(cssText)) + elif self._tokenvalue(selectortokens[0]).startswith(u'@'): + self._log.error(u'CSSStyleRule: No style rule: %r' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + newSelectorList = SelectorList(parentRule=self) + newStyle = CSSStyleDeclaration(parentRule=self) + ok = True + + bracetoken = selectortokens.pop() + if self._tokenvalue(bracetoken) != u'{': + ok = False + self._log.error( + u'CSSStyleRule: No start { of style declaration found: %r' % + self._valuestr(cssText), bracetoken) + elif not selectortokens: + ok = False + self._log.error(u'CSSStyleRule: No selector found: %r.' % + self._valuestr(cssText), bracetoken) + # SET + newSelectorList.selectorText = (selectortokens, + namespaces) + + if not styletokens: + ok = False + self._log.error( + u'CSSStyleRule: No style declaration or "}" found: %r' % + self._valuestr(cssText)) + else: + braceorEOFtoken = styletokens.pop() + val, typ = self._tokenvalue(braceorEOFtoken),\ + self._type(braceorEOFtoken) + if val != u'}' and typ != 'EOF': + ok = False + self._log.error(u'CSSStyleRule: No "}" after style ' + u'declaration found: %r' + % self._valuestr(cssText)) + else: + if 'EOF' == typ: + # add again as style needs it + styletokens.append(braceorEOFtoken) + # SET, may raise: + newStyle.cssText = styletokens + + if ok: + self.selectorList = newSelectorList + self.style = newStyle + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + def __getNamespaces(self): + """Uses children namespaces if not attached to a sheet, else the sheet's + ones.""" + try: + return self.parentStyleSheet.namespaces + except AttributeError: + return self.selectorList._namespaces + + _namespaces = property(__getNamespaces, + doc=u"If this Rule is attached to a CSSStyleSheet " + u"the namespaces of that sheet are mirrored " + u"here. While the Rule is not attached the " + u"namespaces of selectorList are used.""") + + def _setSelectorList(self, selectorList): + """ + :param selectorList: A SelectorList which replaces the current + selectorList object + """ + self._checkReadonly() + selectorList._parentRule = self + self._selectorList = selectorList + + _selectorList = None + selectorList = property(lambda self: self._selectorList, _setSelectorList, + doc=u"The SelectorList of this rule.") + + def _setSelectorText(self, selectorText): + """ + wrapper for cssutils SelectorList object + + :param selectorText: + of type string, might also be a comma separated list + of selectors + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + sl = SelectorList(selectorText=selectorText, parentRule=self) + if sl.wellformed: + self._selectorList = sl + + selectorText = property(lambda self: self._selectorList.selectorText, + _setSelectorText, + doc=u"(DOM) The textual representation of the " + u"selector for the rule set.") + + def _setStyle(self, style): + """ + :param style: A string or CSSStyleDeclaration which replaces the + current style object. + """ + self._checkReadonly() + if isinstance(style, basestring): + self._style = CSSStyleDeclaration(cssText=style, parentRule=self) + else: + style._parentRule = self + self._style = style + + style = property(lambda self: self._style, _setStyle, + doc=u"(DOM) The declaration-block of this rule set.") + + type = property(lambda self: self.STYLE_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + "type constant.") + + wellformed = property(lambda self: self.selectorList.wellformed) diff --git a/libs/cssutils/css/cssstylesheet.py b/libs/cssutils/css/cssstylesheet.py new file mode 100755 index 00000000..88c46b8c --- /dev/null +++ b/libs/cssutils/css/cssstylesheet.py @@ -0,0 +1,804 @@ +"""CSSStyleSheet implements DOM Level 2 CSS CSSStyleSheet. + +Partly also: + - http://dev.w3.org/csswg/cssom/#the-cssstylesheet + - http://www.w3.org/TR/2006/WD-css3-namespace-20060828/ + +TODO: + - ownerRule and ownerNode +""" +__all__ = ['CSSStyleSheet'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.helper import Deprecated +from cssutils.util import _Namespaces, _SimpleNamespaces, _readUrl +from cssrule import CSSRule +from cssvariablesdeclaration import CSSVariablesDeclaration +import cssutils.stylesheets +import xml.dom + +class CSSStyleSheet(cssutils.stylesheets.StyleSheet): + """CSSStyleSheet represents a CSS style sheet. + + Format:: + + stylesheet + : [ CHARSET_SYM S* STRING S* ';' ]? + [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* + [ namespace [S|CDO|CDC]* ]* # according to @namespace WD + [ [ ruleset | media | page ] [S|CDO|CDC]* ]* + + ``cssRules`` + All Rules in this style sheet, a :class:`~cssutils.css.CSSRuleList`. + """ + def __init__(self, href=None, media=None, title=u'', disabled=None, + ownerNode=None, parentStyleSheet=None, readonly=False, + ownerRule=None, + validating=True): + """ + For parameters see :class:`~cssutils.stylesheets.StyleSheet` + """ + super(CSSStyleSheet, self).__init__( + 'text/css', href, media, title, disabled, + ownerNode, parentStyleSheet, + validating=validating) + + self._ownerRule = ownerRule + self.cssRules = cssutils.css.CSSRuleList() + self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log) + self._variables = CSSVariablesDeclaration() + self._readonly = readonly + + # used only during setting cssText by parse*() + self.__encodingOverride = None + self._fetcher = None + + def __iter__(self): + "Generator which iterates over cssRules." + for rule in self._cssRules: + yield rule + + def __repr__(self): + if self.media: + mediaText = self.media.mediaText + else: + mediaText = None + return "cssutils.css.%s(href=%r, media=%r, title=%r)" % ( + self.__class__.__name__, + self.href, mediaText, self.title) + + def __str__(self): + if self.media: + mediaText = self.media.mediaText + else: + mediaText = None + return "" % ( + self.__class__.__name__, self.encoding, self.href, + mediaText, self.title, self.namespaces.namespaces, + id(self)) + + def _cleanNamespaces(self): + "Remove all namespace rules with same namespaceURI but last." + rules = self.cssRules + namespaceitems = self.namespaces.items() + i = 0 + while i < len(rules): + rule = rules[i] + if rule.type == rule.NAMESPACE_RULE and \ + (rule.prefix, rule.namespaceURI) not in namespaceitems: + self.deleteRule(i) + else: + i += 1 + + def _getUsedURIs(self): + "Return set of URIs used in the sheet." + useduris = set() + for r1 in self: + if r1.STYLE_RULE == r1.type: + useduris.update(r1.selectorList._getUsedUris()) + elif r1.MEDIA_RULE == r1.type: + for r2 in r1: + if r2.type == r2.STYLE_RULE: + useduris.update(r2.selectorList._getUsedUris()) + return useduris + + def _setCssRules(self, cssRules): + "Set new cssRules and update contained rules refs." + cssRules.append = self.insertRule + cssRules.extend = self.insertRule + cssRules.__delitem__ = self.deleteRule + + for rule in cssRules: + rule._parentStyleSheet = self + + self._cssRules = cssRules + + cssRules = property(lambda self: self._cssRules, _setCssRules, + u"All Rules in this style sheet, a " + u":class:`~cssutils.css.CSSRuleList`.") + + def _getCssText(self): + "Textual representation of the stylesheet (a byte string)." + return cssutils.ser.do_CSSStyleSheet(self) + + def _setCssText(self, cssText): + """Parse `cssText` and overwrites the whole stylesheet. + + :param cssText: + a parseable string or a tuple of (cssText, dict-of-namespaces) + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + If a namespace prefix is found which is not declared. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + """ + self._checkReadonly() + + cssText, namespaces = self._splitNamespacesOff(cssText) + tokenizer = self._tokenize2(cssText) + + def S(expected, seq, token, tokenizer=None): + # @charset must be at absolute beginning of style sheet + # or 0 for py3 + return max(1, expected or 0) + + def COMMENT(expected, seq, token, tokenizer=None): + "special: sets parent*" + self.insertRule(cssutils.css.CSSComment([token], + parentStyleSheet=self)) + # or 0 for py3 + return max(1, expected or 0) + + def charsetrule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSCharsetRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + + if expected > 0: + self._log.error(u'CSSStylesheet: CSSCharsetRule only allowed ' + u'at beginning of stylesheet.', + token, xml.dom.HierarchyRequestErr) + return expected + elif rule.wellformed: + self.insertRule(rule) + + return 1 + + def importrule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSImportRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + + if expected > 1: + self._log.error(u'CSSStylesheet: CSSImportRule not allowed ' + u'here.', token, xml.dom.HierarchyRequestErr) + return expected + elif rule.wellformed: + self.insertRule(rule) + + return 1 + + def namespacerule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSNamespaceRule(cssText=self._tokensupto2(tokenizer, + token), + parentStyleSheet=self) + + if expected > 2: + self._log.error(u'CSSStylesheet: CSSNamespaceRule not allowed ' + u'here.', token, xml.dom.HierarchyRequestErr) + return expected + elif rule.wellformed: + if rule.prefix not in self.namespaces: + # add new if not same prefix + self.insertRule(rule, _clean=False) + else: + # same prefix => replace namespaceURI + for r in self.cssRules.rulesOfType(rule.NAMESPACE_RULE): + if r.prefix == rule.prefix: + r._replaceNamespaceURI(rule.namespaceURI) + + self._namespaces[rule.prefix] = rule.namespaceURI + + return 2 + + def variablesrule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSVariablesRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + + if expected > 2: + self._log.error(u'CSSStylesheet: CSSVariablesRule not allowed ' + u'here.', token, xml.dom.HierarchyRequestErr) + return expected + elif rule.wellformed: + self.insertRule(rule) + self._updateVariables() + + return 2 + + def fontfacerule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return 3 + + def mediarule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSMediaRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return 3 + + def pagerule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSPageRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return 3 + + def unknownrule(expected, seq, token, tokenizer): + # parse and consume tokens in any case + if token[1] in cssutils.css.MarginRule.margins: + self._log.error(u'CSSStylesheet: MarginRule out CSSPageRule.', + token, neverraise=True) + rule = cssutils.css.MarginRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + else: + self._log.warn(u'CSSStylesheet: Unknown @rule found.', + token, neverraise=True) + rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + + if rule.wellformed: + self.insertRule(rule) + + # or 0 for py3 + return max(1, expected or 0) + + def ruleset(expected, seq, token, tokenizer): + # parse and consume tokens in any case + rule = cssutils.css.CSSStyleRule(parentStyleSheet=self) + rule.cssText = self._tokensupto2(tokenizer, token) + if rule.wellformed: + self.insertRule(rule) + return 3 + + # save for possible reset + oldCssRules = self.cssRules + oldNamespaces = self._namespaces + + self.cssRules = cssutils.css.CSSRuleList() + # simple during parse + self._namespaces = namespaces + self._variables = CSSVariablesDeclaration() + + # not used?! + newseq = [] + + # ['CHARSET', 'IMPORT', ('VAR', NAMESPACE'), ('PAGE', 'MEDIA', ruleset)] + wellformed, expected = self._parse(0, newseq, tokenizer, + {'S': S, + 'COMMENT': COMMENT, + 'CDO': lambda *ignored: None, + 'CDC': lambda *ignored: None, + 'CHARSET_SYM': charsetrule, + 'FONT_FACE_SYM': fontfacerule, + 'IMPORT_SYM': importrule, + 'NAMESPACE_SYM': namespacerule, + 'PAGE_SYM': pagerule, + 'MEDIA_SYM': mediarule, + 'VARIABLES_SYM': variablesrule, + 'ATKEYWORD': unknownrule + }, + default=ruleset) + + if wellformed: + # use proper namespace object + self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log) + self._cleanNamespaces() + + else: + # reset + self._cssRules = oldCssRules + self._namespaces = oldNamespaces + self._updateVariables() + self._cleanNamespaces() + + cssText = property(_getCssText, _setCssText, + "Textual representation of the stylesheet (a byte string)") + + def _resolveImport(self, url): + """Read (encoding, enctype, decodedContent) from `url` for @import + sheets.""" + try: + # only available during parsing of a complete sheet + parentEncoding = self.__newEncoding + + except AttributeError: + try: + # explicit @charset + parentEncoding = self._cssRules[0].encoding + except (IndexError, AttributeError): + # default not UTF-8 but None! + parentEncoding = None + + + return _readUrl(url, fetcher=self._fetcher, + overrideEncoding=self.__encodingOverride, + parentEncoding=parentEncoding) + + def _setCssTextWithEncodingOverride(self, cssText, encodingOverride=None, + encoding=None): + """Set `cssText` but use `encodingOverride` to overwrite detected + encoding. This is used by parse and @import during setting of cssText. + + If `encoding` is given use this but do not save as `encodingOverride`. + """ + if encodingOverride: + # encoding during resolving of @import + self.__encodingOverride = encodingOverride + + if encoding: + # save for nested @import + self.__newEncoding = encoding + + self.cssText = cssText + + if encodingOverride: + # set encodingOverride explicit again! + self.encoding = self.__encodingOverride + # del? + self.__encodingOverride = None + elif encoding: + # may e.g. be httpEncoding + self.encoding = encoding + try: + del self.__newEncoding + except AttributeError, e: + pass + + def _setFetcher(self, fetcher=None): + """Set @import URL loader, if None the default is used.""" + self._fetcher = fetcher + + def _getEncoding(self): + """Encoding set in :class:`~cssutils.css.CSSCharsetRule` or if ``None`` + resulting in default ``utf-8`` encoding being used.""" + try: + return self._cssRules[0].encoding + except (IndexError, AttributeError): + return 'utf-8' + + def _setEncoding(self, encoding): + """Set `encoding` of charset rule if present in sheet or insert a new + :class:`~cssutils.css.CSSCharsetRule` with given `encoding`. + If `encoding` is None removes charsetrule if present resulting in + default encoding of utf-8. + """ + try: + rule = self._cssRules[0] + except IndexError: + rule = None + if rule and rule.CHARSET_RULE == rule.type: + if encoding: + rule.encoding = encoding + else: + self.deleteRule(0) + elif encoding: + self.insertRule(cssutils.css.CSSCharsetRule(encoding=encoding), 0) + + encoding = property(_getEncoding, _setEncoding, + "(cssutils) Reflect encoding of an @charset rule or 'utf-8' " + "(default) if set to ``None``") + + namespaces = property(lambda self: self._namespaces, + doc="All Namespaces used in this CSSStyleSheet.") + + def _updateVariables(self): + """Updates self._variables, called when @import or @variables rules + is added to sheet. + """ + for r in self.cssRules.rulesOfType(CSSRule.IMPORT_RULE): + s = r.styleSheet + if s: + for var in s.variables: + self._variables.setVariable(var, s.variables[var]) +# for r in self.cssRules.rulesOfType(CSSRule.IMPORT_RULE): +# for vr in r.styleSheet.cssRules.rulesOfType(CSSRule.VARIABLES_RULE): +# for var in vr.variables: +# self._variables.setVariable(var, vr.variables[var]) + for vr in self.cssRules.rulesOfType(CSSRule.VARIABLES_RULE): + for var in vr.variables: + self._variables.setVariable(var, vr.variables[var]) + + variables = property(lambda self: self._variables, + doc=u"A :class:`cssutils.css.CSSVariablesDeclaration` " + u"containing all available variables in this " + u"CSSStyleSheet including the ones defined in " + u"imported sheets.") + + def add(self, rule): + """Add `rule` to style sheet at appropriate position. + Same as ``insertRule(rule, inOrder=True)``. + """ + return self.insertRule(rule, index=None, inOrder=True) + + def deleteRule(self, index): + """Delete rule at `index` from the style sheet. + + :param index: + The `index` of the rule to be removed from the StyleSheet's rule + list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is + raised but rules for normal Python lists are used. E.g. + ``deleteRule(-1)`` removes the last rule in cssRules. + + `index` may also be a CSSRule object which will then be removed + from the StyleSheet. + + :exceptions: + - :exc:`~xml.dom.IndexSizeErr`: + Raised if the specified index does not correspond to a rule in + the style sheet's rule list. + - :exc:`~xml.dom.NamespaceErr`: + Raised if removing this rule would result in an invalid StyleSheet + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this style sheet is readonly. + """ + self._checkReadonly() + + if isinstance(index, CSSRule): + for i, r in enumerate(self.cssRules): + if index == r: + index = i + break + else: + raise xml.dom.IndexSizeErr(u"CSSStyleSheet: Not a rule in" + " this sheets'a cssRules list: %s" + % index) + + try: + rule = self._cssRules[index] + except IndexError: + raise xml.dom.IndexSizeErr( + u'CSSStyleSheet: %s is not a valid index in the rulelist of ' + u'length %i' % (index, self._cssRules.length)) + else: + if rule.type == rule.NAMESPACE_RULE: + # check all namespacerules if used + uris = [r.namespaceURI for r in self + if r.type == r.NAMESPACE_RULE] + useduris = self._getUsedURIs() + if rule.namespaceURI in useduris and\ + uris.count(rule.namespaceURI) == 1: + raise xml.dom.NoModificationAllowedErr( + u'CSSStyleSheet: NamespaceURI defined in this rule is ' + u'used, cannot remove.') + return + + rule._parentStyleSheet = None # detach + del self._cssRules[index] # delete from StyleSheet + + def insertRule(self, rule, index=None, inOrder=False, _clean=True): + """ + Used to insert a new rule into the style sheet. The new rule now + becomes part of the cascade. + + :param rule: + a parsable DOMString, in cssutils also a + :class:`~cssutils.css.CSSRule` or :class:`~cssutils.css.CSSRuleList` + :param index: + of the rule before the new rule will be inserted. + If the specified `index` is equal to the length of the + StyleSheet's rule collection, the rule will be added to the end + of the style sheet. + If `index` is not given or ``None`` rule will be appended to rule + list. + :param inOrder: + if ``True`` the rule will be put to a proper location while + ignoring `index` and without raising + :exc:`~xml.dom.HierarchyRequestErr`. + The resulting index is returned nevertheless. + :returns: The index within the style sheet's rule collection + :Exceptions: + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at the specified `index` + e.g. if an @import rule is inserted after a standard rule set + or other at-rule. + - :exc:`~xml.dom.IndexSizeErr`: + Raised if the specified `index` is not a valid insertion point. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this style sheet is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified rule has a syntax error and is + unparsable. + """ + self._checkReadonly() + + # check position + if index is None: + index = len(self._cssRules) + elif index < 0 or index > self._cssRules.length: + raise xml.dom.IndexSizeErr( + u'CSSStyleSheet: Invalid index %s for CSSRuleList with a ' + u'length of %s.' % (index, self._cssRules.length)) + return + + if isinstance(rule, basestring): + # init a temp sheet which has the same properties as self + tempsheet = CSSStyleSheet(href=self.href, + media=self.media, + title=self.title, + parentStyleSheet=self.parentStyleSheet, + ownerRule=self.ownerRule) + tempsheet._ownerNode = self.ownerNode + tempsheet._fetcher = self._fetcher + # prepend encoding if in this sheet to be able to use it in + # @import rules encoding resolution + # do not add if new rule startswith "@charset" (which is exact!) + if not rule.startswith(u'@charset') and (self._cssRules and + self._cssRules[0].type == self._cssRules[0].CHARSET_RULE): + # rule 0 is @charset! + newrulescount, newruleindex = 2, 1 + rule = self._cssRules[0].cssText + rule + else: + newrulescount, newruleindex = 1, 0 + + # parse the new rule(s) + tempsheet.cssText = (rule, self._namespaces) + + if len(tempsheet.cssRules) != newrulescount or (not isinstance( + tempsheet.cssRules[newruleindex], cssutils.css.CSSRule)): + self._log.error(u'CSSStyleSheet: Not a CSSRule: %s' % rule) + return + rule = tempsheet.cssRules[newruleindex] + rule._parentStyleSheet = None # done later? + + # TODO: + #tempsheet._namespaces = self._namespaces + #variables? + + elif isinstance(rule, cssutils.css.CSSRuleList): + # insert all rules + for i, r in enumerate(rule): + self.insertRule(r, index + i) + return index + + if not rule.wellformed: + self._log.error(u'CSSStyleSheet: Invalid rules cannot be added.') + return + + # CHECK HIERARCHY + # @charset + if rule.type == rule.CHARSET_RULE: + if inOrder: + index = 0 + # always first and only + if (self._cssRules + and self._cssRules[0].type == rule.CHARSET_RULE): + self._cssRules[0].encoding = rule.encoding + else: + self._cssRules.insert(0, rule) + elif index != 0 or (self._cssRules and + self._cssRules[0].type == rule.CHARSET_RULE): + self._log.error( + u'CSSStylesheet: @charset only allowed once at the' + ' beginning of a stylesheet.', + error=xml.dom.HierarchyRequestErr) + return + else: + self._cssRules.insert(index, rule) + + # @unknown or comment + elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder: + if index == 0 and self._cssRules and\ + self._cssRules[0].type == rule.CHARSET_RULE: + self._log.error( + u'CSSStylesheet: @charset must be the first rule.', + error=xml.dom.HierarchyRequestErr) + return + else: + self._cssRules.insert(index, rule) + + # @import + elif rule.type == rule.IMPORT_RULE: + if inOrder: + # automatic order + if rule.type in (r.type for r in self): + # find last of this type + for i, r in enumerate(reversed(self._cssRules)): + if r.type == rule.type: + index = len(self._cssRules) - i + break + else: + # find first point to insert + if self._cssRules and\ + self._cssRules[0].type in (rule.CHARSET_RULE, + rule.COMMENT): + index = 1 + else: + index = 0 + else: + # after @charset + if index == 0 and self._cssRules and\ + self._cssRules[0].type == rule.CHARSET_RULE: + self._log.error( + u'CSSStylesheet: Found @charset at index 0.', + error=xml.dom.HierarchyRequestErr) + return + # before @namespace @variables @page @font-face @media stylerule + for r in self._cssRules[:index]: + if r.type in (r.NAMESPACE_RULE, + r.VARIABLES_RULE, + r.MEDIA_RULE, + r.PAGE_RULE, + r.STYLE_RULE, + r.FONT_FACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @import here,' + ' found @namespace, @variables, @media, @page or' + ' CSSStyleRule before index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + self._cssRules.insert(index, rule) + self._updateVariables() + + # @namespace + elif rule.type == rule.NAMESPACE_RULE: + if inOrder: + if rule.type in (r.type for r in self): + # find last of this type + for i, r in enumerate(reversed(self._cssRules)): + if r.type == rule.type: + index = len(self._cssRules) - i + break + else: + # find first point to insert + for i, r in enumerate(self._cssRules): + if r.type in (r.VARIABLES_RULE, r.MEDIA_RULE, + r.PAGE_RULE, r.STYLE_RULE, + r.FONT_FACE_RULE, r.UNKNOWN_RULE, + r.COMMENT): + index = i # before these + break + else: + # after @charset and @import + for r in self._cssRules[index:]: + if r.type in (r.CHARSET_RULE, r.IMPORT_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @namespace here,' + ' found @charset or @import after index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + # before @variables @media @page @font-face and stylerule + for r in self._cssRules[:index]: + if r.type in (r.VARIABLES_RULE, + r.MEDIA_RULE, + r.PAGE_RULE, + r.STYLE_RULE, + r.FONT_FACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @namespace here,' + ' found @variables, @media, @page or CSSStyleRule' + ' before index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + + if not (rule.prefix in self.namespaces and + self.namespaces[rule.prefix] == rule.namespaceURI): + # no doublettes + self._cssRules.insert(index, rule) + if _clean: + self._cleanNamespaces() + + + # @variables + elif rule.type == rule.VARIABLES_RULE: + if inOrder: + if rule.type in (r.type for r in self): + # find last of this type + for i, r in enumerate(reversed(self._cssRules)): + if r.type == rule.type: + index = len(self._cssRules) - i + break + else: + # find first point to insert + for i, r in enumerate(self._cssRules): + if r.type in (r.MEDIA_RULE, + r.PAGE_RULE, + r.STYLE_RULE, + r.FONT_FACE_RULE, + r.UNKNOWN_RULE, + r.COMMENT): + index = i # before these + break + else: + # after @charset @import @namespace + for r in self._cssRules[index:]: + if r.type in (r.CHARSET_RULE, + r.IMPORT_RULE, + r.NAMESPACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @variables here,' + ' found @charset, @import or @namespace after' + ' index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + # before @media @page @font-face and stylerule + for r in self._cssRules[:index]: + if r.type in (r.MEDIA_RULE, + r.PAGE_RULE, + r.STYLE_RULE, + r.FONT_FACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert @variables here,' + ' found @media, @page or CSSStyleRule' + ' before index %s.' % + index, + error=xml.dom.HierarchyRequestErr) + return + + self._cssRules.insert(index, rule) + self._updateVariables() + + # all other where order is not important + else: + if inOrder: + # simply add to end as no specific order + self._cssRules.append(rule) + index = len(self._cssRules) - 1 + else: + for r in self._cssRules[index:]: + if r.type in (r.CHARSET_RULE, + r.IMPORT_RULE, + r.NAMESPACE_RULE): + self._log.error( + u'CSSStylesheet: Cannot insert rule here, found ' + u'@charset, @import or @namespace before index %s.' + % index, error=xml.dom.HierarchyRequestErr) + return + self._cssRules.insert(index, rule) + + # post settings + rule._parentStyleSheet = self + + if rule.IMPORT_RULE == rule.type and not rule.hrefFound: + # try loading the imported sheet which has new relative href now + rule.href = rule.href + + return index + + ownerRule = property(lambda self: self._ownerRule, + doc=u'A ref to an @import rule if it is imported, ' + u'else ``None``.') + + + @Deprecated(u'Use ``cssutils.setSerializer(serializer)`` instead.') + def setSerializer(self, cssserializer): + """Set the cssutils global Serializer used for all output.""" + if isinstance(cssserializer, cssutils.CSSSerializer): + cssutils.ser = cssserializer + else: + raise ValueError(u'Serializer must be an instance of ' + u'cssutils.CSSSerializer.') + + @Deprecated(u'Set pref in ``cssutils.ser.prefs`` instead.') + def setSerializerPref(self, pref, value): + """Set a Preference of CSSSerializer used for output. + See :class:`cssutils.serialize.Preferences` for possible + preferences to be set. + """ + cssutils.ser.prefs.__setattr__(pref, value) diff --git a/libs/cssutils/css/cssunknownrule.py b/libs/cssutils/css/cssunknownrule.py new file mode 100755 index 00000000..c9e4361f --- /dev/null +++ b/libs/cssutils/css/cssunknownrule.py @@ -0,0 +1,209 @@ +"""CSSUnknownRule implements DOM Level 2 CSS CSSUnknownRule.""" +__all__ = ['CSSUnknownRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import cssrule +import cssutils +import xml.dom + +class CSSUnknownRule(cssrule.CSSRule): + """ + Represents an at-rule not supported by this user agent, so in + effect all other at-rules not defined in cssutils. + + Format:: + + @xxx until ';' or block {...} + """ + def __init__(self, cssText=u'', parentRule=None, + parentStyleSheet=None, readonly=False): + """ + :param cssText: + of type string + """ + super(CSSUnknownRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = None + if cssText: + self.cssText = cssText + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(cssText=%r)" % ( + self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.cssText, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSUnknownRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(CSSUnknownRule, self)._setCssText(cssText) + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if not attoken or self._type(attoken) != self._prods.ATKEYWORD: + self._log.error(u'CSSUnknownRule: No CSSUnknownRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + # for closures: must be a mutable + new = {'nesting': [], # {} [] or () + 'wellformed': True + } + + def CHAR(expected, seq, token, tokenizer=None): + type_, val, line, col = token + if expected != 'EOF': + if val in u'{[(': + new['nesting'].append(val) + elif val in u'}])': + opening = {u'}': u'{', u']': u'[', u')': u'('}[val] + try: + if new['nesting'][-1] == opening: + new['nesting'].pop() + else: + raise IndexError() + except IndexError: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Wrong nesting of ' + u'{, [ or (.', token=token) + + if val in u'};' and not new['nesting']: + expected = 'EOF' + + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + def FUNCTION(expected, seq, token, tokenizer=None): + # handled as opening ( + type_, val, line, col = token + val = self._tokenvalue(token) + if expected != 'EOF': + new['nesting'].append(u'(') + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + def EOF(expected, seq, token, tokenizer=None): + "close all blocks and return 'EOF'" + for x in reversed(new['nesting']): + closing = {u'{': u'}', u'[': u']', u'(': u')'}[x] + seq.append(closing, closing) + new['nesting'] = [] + return 'EOF' + + def INVALID(expected, seq, token, tokenizer=None): + # makes rule invalid + self._log.error(u'CSSUnknownRule: Bad syntax.', + token=token, error=xml.dom.SyntaxErr) + new['wellformed'] = False + return expected + + def STRING(expected, seq, token, tokenizer=None): + type_, val, line, col = token + val = self._stringtokenvalue(token) + if expected != 'EOF': + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + def URI(expected, seq, token, tokenizer=None): + type_, val, line, col = token + val = self._uritokenvalue(token) + if expected != 'EOF': + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + def default(expected, seq, token, tokenizer=None): + type_, val, line, col = token + if expected != 'EOF': + seq.append(val, type_, line=line, col=col) + return expected + else: + new['wellformed'] = False + self._log.error(u'CSSUnknownRule: Expected end of rule.', + token=token) + return expected + + # unknown : ATKEYWORD S* ... ; | } + newseq = self._tempSeq() + wellformed, expected = self._parse(expected=None, + seq=newseq, tokenizer=tokenizer, + productions={'CHAR': CHAR, + 'EOF': EOF, + 'FUNCTION': FUNCTION, + 'INVALID': INVALID, + 'STRING': STRING, + 'URI': URI, + 'S': default # overwrite default default! + }, + default=default, + new=new) + + # wellformed set by parse + wellformed = wellformed and new['wellformed'] + + # post conditions + if expected != 'EOF': + wellformed = False + self._log.error(u'CSSUnknownRule: No ending ";" or "}" found: ' + u'%r' % self._valuestr(cssText)) + elif new['nesting']: + wellformed = False + self._log.error(u'CSSUnknownRule: Unclosed "{", "[" or "(": %r' + % self._valuestr(cssText)) + + # set all + if wellformed: + self.atkeyword = self._tokenvalue(attoken) + self._setSeq(newseq) + + cssText = property(fget=_getCssText, fset=_setCssText, + doc=u"(DOM) The parsable textual representation.") + + type = property(lambda self: self.UNKNOWN_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: bool(self.atkeyword)) + \ No newline at end of file diff --git a/libs/cssutils/css/cssvalue.py b/libs/cssutils/css/cssvalue.py new file mode 100755 index 00000000..2f643453 --- /dev/null +++ b/libs/cssutils/css/cssvalue.py @@ -0,0 +1,1251 @@ +"""CSSValue related classes + +- CSSValue implements DOM Level 2 CSS CSSValue +- CSSPrimitiveValue implements DOM Level 2 CSS CSSPrimitiveValue +- CSSValueList implements DOM Level 2 CSS CSSValueList + +""" +__all__ = ['CSSValue', 'CSSPrimitiveValue', 'CSSValueList', 'RGBColor', + 'CSSVariable'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.prodparser import * +import cssutils +import cssutils.helper +import math +import re +import xml.dom + + +class CSSValue(cssutils.util._NewBase): + """The CSSValue interface represents a simple or a complex value. + A CSSValue object only occurs in a context of a CSS property. + """ + + # The value is inherited and the cssText contains "inherit". + CSS_INHERIT = 0 + # The value is a CSSPrimitiveValue. + CSS_PRIMITIVE_VALUE = 1 + # The value is a CSSValueList. + CSS_VALUE_LIST = 2 + # The value is a custom value. + CSS_CUSTOM = 3 + # The value is a CSSVariable. + CSS_VARIABLE = 4 + + _typestrings = {0: 'CSS_INHERIT' , + 1: 'CSS_PRIMITIVE_VALUE', + 2: 'CSS_VALUE_LIST', + 3: 'CSS_CUSTOM', + 4: 'CSS_VARIABLE'} + + def __init__(self, cssText=None, parent=None, readonly=False): + """ + :param cssText: + the parsable cssText of the value + :param readonly: + defaults to False + """ + super(CSSValue, self).__init__() + + self._cssValueType = None + self.wellformed = False + self.parent = parent + if cssText is not None: # may be 0 + if isinstance(cssText, int): + cssText = unicode(cssText) # if it is an integer + elif isinstance(cssText, float): + cssText = u'%f' % cssText # if it is a floating point number + + self.cssText = cssText + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.cssValueTypeString, + self.cssText, + id(self)) + + def _setCssText(self, cssText): + """ + Format:: + + unary_operator + : '-' | '+' + ; + operator + : '/' S* | ',' S* | /* empty */ + ; + expr + : term [ operator term ]* + ; + term + : unary_operator? + [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | + ANGLE S* | TIME S* | FREQ S* ] + | STRING S* | IDENT S* | URI S* | hexcolor | function + | UNICODE-RANGE S* + ; + function + : FUNCTION S* expr ')' S* + ; + /* + * There is a constraint on the color that it must + * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) + * after the "#"; e.g., "#000" is OK, but "#abcd" is not. + */ + hexcolor + : HASH S* + ; + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + (according to the attached property) or is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + TODO: Raised if the specified CSS string value represents a + different type of values than the values allowed by the CSS + property. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this value is readonly. + """ + self._checkReadonly() + + # used as operator is , / or S + nextSor = u',/' + + term = Choice(Sequence(PreDef.unary(), + Choice(PreDef.number(nextSor=nextSor), + PreDef.percentage(nextSor=nextSor), + PreDef.dimension(nextSor=nextSor))), + PreDef.string(nextSor=nextSor), + PreDef.ident(nextSor=nextSor), + PreDef.uri(nextSor=nextSor), + PreDef.hexcolor(nextSor=nextSor), + PreDef.unicode_range(nextSor=nextSor), + # special case IE only expression + Prod(name='expression', + match=lambda t, v: t == self._prods.FUNCTION and ( + cssutils.helper.normalize(v) in (u'expression(', + u'alpha(', + u'blur(', + u'chroma(', + u'dropshadow(', + u'fliph(', + u'flipv(', + u'glow(', + u'gray(', + u'invert(', + u'mask(', + u'shadow(', + u'wave(', + u'xray(') or + v.startswith(u'progid:DXImageTransform.Microsoft.') + ), + nextSor=nextSor, + toSeq=lambda t, tokens: (ExpressionValue._functionName, + ExpressionValue( + cssutils.helper.pushtoken(t, tokens), + parent=self) + ) + ), + # CSS Variable var( + PreDef.variable(nextSor=nextSor, + toSeq=lambda t, tokens: ('CSSVariable', + CSSVariable( + cssutils.helper.pushtoken(t, tokens), + parent=self) + ) + ), + # calc( + PreDef.calc(nextSor=nextSor, + toSeq=lambda t, tokens: (CalcValue._functionName, + CalcValue( + cssutils.helper.pushtoken(t, tokens), + parent=self) + ) + ), +# TODO: +# # rgb/rgba( +# Prod(name='RGBColor', +# match=lambda t, v: t == self._prods.FUNCTION and ( +# cssutils.helper.normalize(v) in (u'rgb(', +# u'rgba(' +# ) +# ), +# nextSor=nextSor, +# toSeq=lambda t, tokens: (RGBColor._functionName, +# RGBColor( +# cssutils.helper.pushtoken(t, tokens), +# parent=self) +# ) +# ), + # other functions like rgb( etc + PreDef.function(nextSor=nextSor, + toSeq=lambda t, tokens: ('FUNCTION', + CSSFunction( + cssutils.helper.pushtoken(t, tokens), + parent=self) + ) + ) + ) + operator = Choice(PreDef.S(), + PreDef.char('comma', ',', + toSeq=lambda t, tokens: ('operator', t[1])), + PreDef.char('slash', '/', + toSeq=lambda t, tokens: ('operator', t[1])), + optional=True) + # CSSValue PRODUCTIONS + valueprods = Sequence(term, + Sequence(operator, # mayEnd this Sequence if whitespace + + # TODO: only when setting via other class + # used by variabledeclaration currently + PreDef.char('END', ';', + stopAndKeep=True, + optional=True), + + term, + minmax=lambda: (0, None))) + # parse + wellformed, seq, store, notused = ProdParser().parse(cssText, + u'CSSValue', + valueprods, + keepS=True) + if wellformed: + # - count actual values and set firstvalue which is used later on + # - combine comma separated list, e.g. font-family to a single item + # - remove S which should be an operator but is no needed + count, firstvalue = 0, () + newseq = self._tempSeq() + i, end = 0, len(seq) + while i < end: + item = seq[i] + if item.type == self._prods.S: + pass + + elif (item.value, item.type) == (u',', 'operator'): + # , separared counts as a single STRING for now + # URI or STRING value might be a single CHAR too! + newseq.appendItem(item) + count -= 1 + if firstvalue: + # list of IDENTs is handled as STRING! + if firstvalue[1] == self._prods.IDENT: + firstvalue = firstvalue[0], 'STRING' + + elif item.value == u'/': + # / separated items count as one + newseq.appendItem(item) + + elif item.value == u'-' or item.value == u'+': + # combine +- and following number or other + i += 1 + try: + next = seq[i] + except IndexError: + firstvalue = () # raised later + break + + newval = item.value + next.value + newseq.append(newval, next.type, + item.line, item.col) + if not firstvalue: + firstvalue = (newval, next.type) + count += 1 + + elif item.type != cssutils.css.CSSComment: + newseq.appendItem(item) + if not firstvalue: + firstvalue = (item.value, item.type) + count += 1 + + else: + newseq.appendItem(item) + + i += 1 + + if not firstvalue: + self._log.error( + u'CSSValue: Unknown syntax or no value: %r.' % + self._valuestr(cssText)) + else: + # ok and set + self._setSeq(newseq) + self.wellformed = wellformed + + if hasattr(self, '_value'): + # only in case of CSSPrimitiveValue, else remove! + del self._value + + if count == 1: + # inherit, primitive or variable + if isinstance(firstvalue[0], basestring) and\ + u'inherit' == cssutils.helper.normalize(firstvalue[0]): + self.__class__ = CSSValue + self._cssValueType = CSSValue.CSS_INHERIT + elif 'CSSVariable' == firstvalue[1]: + self.__class__ = CSSVariable + self._value = firstvalue + # TODO: remove major hack! + self._name = firstvalue[0]._name + else: + self.__class__ = CSSPrimitiveValue + self._value = firstvalue + + elif count > 1: + # valuelist + self.__class__ = CSSValueList + + # change items in list to specific type (primitive etc) + newseq = self._tempSeq() + commalist = [] + nexttocommalist = False + + def itemValue(item): + "Reserialized simple item.value" + if self._prods.STRING == item.type: + return cssutils.helper.string(item.value) + elif self._prods.URI == item.type: + return cssutils.helper.uri(item.value) + elif self._prods.FUNCTION == item.type or\ + 'CSSVariable' == item.type: + return item.value.cssText + else: + return item.value + + def saveifcommalist(commalist, newseq): + """ + saves items in commalist to seq and items + if anything in there + """ + if commalist: + newseq.replace(-1, + CSSPrimitiveValue(cssText=u''.join( + commalist)), + CSSPrimitiveValue, + newseq[-1].line, + newseq[-1].col) + del commalist[:] + + for i, item in enumerate(self._seq): + if issubclass(type(item.value), CSSValue): + # set parent of CSSValueList items to the lists + # parent + item.value.parent = self.parent + + if item.type in (self._prods.DIMENSION, + self._prods.FUNCTION, + self._prods.HASH, + self._prods.IDENT, + self._prods.NUMBER, + self._prods.PERCENTAGE, + self._prods.STRING, + self._prods.URI, + self._prods.UNICODE_RANGE, + 'CSSVariable'): + if nexttocommalist: + # wait until complete + commalist.append(itemValue(item)) + else: + saveifcommalist(commalist, newseq) + # append new item + if hasattr(item.value, 'cssText'): + newseq.append(item.value, + item.value.__class__, + item.line, item.col) + + else: + newseq.append(CSSPrimitiveValue( + itemValue(item)), + CSSPrimitiveValue, + item.line, item.col) + + nexttocommalist = False + + elif u',' == item.value: + if not commalist: + # save last item to commalist + commalist.append(itemValue(self._seq[i - 1])) + commalist.append(u',') + nexttocommalist = True + + else: + if nexttocommalist: + commalist.append(item.value.cssText) + else: + newseq.appendItem(item) + + saveifcommalist(commalist, newseq) + self._setSeq(newseq) + + else: + # should not happen... + self.__class__ = CSSValue + self._cssValueType = CSSValue.CSS_CUSTOM + + cssText = property(lambda self: cssutils.ser.do_css_CSSValue(self), + _setCssText, + doc="A string representation of the current value.") + + cssValueType = property(lambda self: self._cssValueType, + doc="A (readonly) code defining the type of the value.") + + cssValueTypeString = property( + lambda self: CSSValue._typestrings.get(self.cssValueType, None), + doc="(readonly) Name of cssValueType.") + + +class CSSPrimitiveValue(CSSValue): + """Represents a single CSS Value. May be used to determine the value of a + specific style property currently set in a block or to set a specific + style property explicitly within the block. Might be obtained from the + getPropertyCSSValue method of CSSStyleDeclaration. + + Conversions are allowed between absolute values (from millimeters to + centimeters, from degrees to radians, and so on) but not between + relative values. (For example, a pixel value cannot be converted to a + centimeter value.) Percentage values can't be converted since they are + relative to the parent value (or another property value). There is one + exception for color percentage values: since a color percentage value + is relative to the range 0-255, a color percentage value can be + converted to a number; (see also the RGBColor interface). + """ + # constant: type of this CSSValue class + cssValueType = CSSValue.CSS_PRIMITIVE_VALUE + + __types = cssutils.cssproductions.CSSProductions + + # An integer indicating which type of unit applies to the value. + CSS_UNKNOWN = 0 # only obtainable via cssText + CSS_NUMBER = 1 + CSS_PERCENTAGE = 2 + CSS_EMS = 3 + CSS_EXS = 4 + CSS_PX = 5 + CSS_CM = 6 + CSS_MM = 7 + CSS_IN = 8 + CSS_PT = 9 + CSS_PC = 10 + CSS_DEG = 11 + CSS_RAD = 12 + CSS_GRAD = 13 + CSS_MS = 14 + CSS_S = 15 + CSS_HZ = 16 + CSS_KHZ = 17 + CSS_DIMENSION = 18 + CSS_STRING = 19 + CSS_URI = 20 + CSS_IDENT = 21 + CSS_ATTR = 22 + CSS_COUNTER = 23 + CSS_RECT = 24 + CSS_RGBCOLOR = 25 + # NOT OFFICIAL: + CSS_RGBACOLOR = 26 + CSS_UNICODE_RANGE = 27 + + _floattypes = (CSS_NUMBER, CSS_PERCENTAGE, CSS_EMS, CSS_EXS, + CSS_PX, CSS_CM, CSS_MM, CSS_IN, CSS_PT, CSS_PC, + CSS_DEG, CSS_RAD, CSS_GRAD, CSS_MS, CSS_S, + CSS_HZ, CSS_KHZ, CSS_DIMENSION) + _stringtypes = (CSS_ATTR, CSS_IDENT, CSS_STRING, CSS_URI) + _countertypes = (CSS_COUNTER,) + _recttypes = (CSS_RECT,) + _rbgtypes = (CSS_RGBCOLOR, CSS_RGBACOLOR) + _lengthtypes = (CSS_NUMBER, CSS_EMS, CSS_EXS, + CSS_PX, CSS_CM, CSS_MM, CSS_IN, CSS_PT, CSS_PC) + + # oldtype: newType: converterfunc + _converter = { + # cm <-> mm <-> in, 1 inch is equal to 2.54 centimeters. + # pt <-> pc, the points used by CSS 2.1 are equal to 1/72nd of an inch. + # pc: picas - 1 pica is equal to 12 points + (CSS_CM, CSS_MM): lambda x: x * 10, + (CSS_MM, CSS_CM): lambda x: x / 10, + + (CSS_PT, CSS_PC): lambda x: x * 12, + (CSS_PC, CSS_PT): lambda x: x / 12, + + (CSS_CM, CSS_IN): lambda x: x / 2.54, + (CSS_IN, CSS_CM): lambda x: x * 2.54, + (CSS_MM, CSS_IN): lambda x: x / 25.4, + (CSS_IN, CSS_MM): lambda x: x * 25.4, + + (CSS_IN, CSS_PT): lambda x: x / 72, + (CSS_PT, CSS_IN): lambda x: x * 72, + (CSS_CM, CSS_PT): lambda x: x / 2.54 / 72, + (CSS_PT, CSS_CM): lambda x: x * 72 * 2.54, + (CSS_MM, CSS_PT): lambda x: x / 25.4 / 72, + (CSS_PT, CSS_MM): lambda x: x * 72 * 25.4, + + (CSS_IN, CSS_PC): lambda x: x / 72 / 12, + (CSS_PC, CSS_IN): lambda x: x * 12 * 72, + (CSS_CM, CSS_PC): lambda x: x / 2.54 / 72 / 12, + (CSS_PC, CSS_CM): lambda x: x * 12 * 72 * 2.54, + (CSS_MM, CSS_PC): lambda x: x / 25.4 / 72 / 12, + (CSS_PC, CSS_MM): lambda x: x * 12 * 72 * 25.4, + + # hz <-> khz + (CSS_KHZ, CSS_HZ): lambda x: x * 1000, + (CSS_HZ, CSS_KHZ): lambda x: x / 1000, + # s <-> ms + (CSS_S, CSS_MS): lambda x: x * 1000, + (CSS_MS, CSS_S): lambda x: x / 1000, + + (CSS_RAD, CSS_DEG): lambda x: math.degrees(x), + (CSS_DEG, CSS_RAD): lambda x: math.radians(x), + # TODO: convert grad <-> deg or rad + #(CSS_RAD, CSS_GRAD): lambda x: math.degrees(x), + #(CSS_DEG, CSS_GRAD): lambda x: math.radians(x), + #(CSS_GRAD, CSS_RAD): lambda x: math.radians(x), + #(CSS_GRAD, CSS_DEG): lambda x: math.radians(x) + } + + def __init__(self, cssText=None, parent=None, readonly=False): + """See CSSPrimitiveValue.__init__()""" + super(CSSPrimitiveValue, self).__init__(cssText=cssText, + parent=parent, + readonly=readonly) + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.primitiveTypeString, + self.cssText, + id(self)) + + _unitnames = ['CSS_UNKNOWN', + 'CSS_NUMBER', 'CSS_PERCENTAGE', + 'CSS_EMS', 'CSS_EXS', + 'CSS_PX', + 'CSS_CM', 'CSS_MM', + 'CSS_IN', + 'CSS_PT', 'CSS_PC', + 'CSS_DEG', 'CSS_RAD', 'CSS_GRAD', + 'CSS_MS', 'CSS_S', + 'CSS_HZ', 'CSS_KHZ', + 'CSS_DIMENSION', + 'CSS_STRING', 'CSS_URI', 'CSS_IDENT', + 'CSS_ATTR', 'CSS_COUNTER', 'CSS_RECT', + 'CSS_RGBCOLOR', 'CSS_RGBACOLOR', + 'CSS_UNICODE_RANGE' + ] + + _reNumDim = re.compile(ur'([+-]?\d*\.\d+|[+-]?\d+)(.*)$', re.I | re.U | re.X) + + def _unitDIMENSION(value): + """Check val for dimension name.""" + units = {'em': 'CSS_EMS', 'ex': 'CSS_EXS', + 'px': 'CSS_PX', + 'cm': 'CSS_CM', 'mm': 'CSS_MM', + 'in': 'CSS_IN', + 'pt': 'CSS_PT', 'pc': 'CSS_PC', + 'deg': 'CSS_DEG', 'rad': 'CSS_RAD', 'grad': 'CSS_GRAD', + 'ms': 'CSS_MS', 's': 'CSS_S', + 'hz': 'CSS_HZ', 'khz': 'CSS_KHZ' + } + val, dim = CSSPrimitiveValue._reNumDim.findall(cssutils.helper.normalize(value))[0] + return units.get(dim, 'CSS_DIMENSION') + + def _unitFUNCTION(value): + """Check val for function name.""" + units = {'attr(': 'CSS_ATTR', + 'counter(': 'CSS_COUNTER', + 'rect(': 'CSS_RECT', + 'rgb(': 'CSS_RGBCOLOR', + 'rgba(': 'CSS_RGBACOLOR', + } + return units.get(re.findall(ur'^(.*?\()', + cssutils.helper.normalize(value.cssText), + re.U)[0], + 'CSS_UNKNOWN') + + __unitbytype = { + __types.NUMBER: 'CSS_NUMBER', + __types.PERCENTAGE: 'CSS_PERCENTAGE', + __types.STRING: 'CSS_STRING', + __types.UNICODE_RANGE: 'CSS_UNICODE_RANGE', + __types.URI: 'CSS_URI', + __types.IDENT: 'CSS_IDENT', + __types.HASH: 'CSS_RGBCOLOR', + __types.DIMENSION: _unitDIMENSION, + __types.FUNCTION: _unitFUNCTION + } + + def __set_primitiveType(self): + """primitiveType is readonly but is set lazy if accessed""" + # TODO: check unary and font-family STRING a, b, "c" + val, type_ = self._value + # try get by type_ + pt = self.__unitbytype.get(type_, 'CSS_UNKNOWN') + if callable(pt): + # multiple options, check value too + pt = pt(val) + self._primitiveType = getattr(self, pt) + + def _getPrimitiveType(self): + if not hasattr(self, '_primitivetype'): + self.__set_primitiveType() + return self._primitiveType + + primitiveType = property(_getPrimitiveType, + doc="(readonly) The type of the value as defined " + "by the constants in this class.") + + def _getPrimitiveTypeString(self): + return self._unitnames[self.primitiveType] + + primitiveTypeString = property(_getPrimitiveTypeString, + doc="Name of primitive type of this value.") + + def _getCSSPrimitiveTypeString(self, type): + "get TypeString by given type which may be unknown, used by setters" + try: + return self._unitnames[type] + except (IndexError, TypeError): + return u'%r (UNKNOWN TYPE)' % type + + def _getNumDim(self, value=None): + "Split self._value in numerical and dimension part." + if value is None: + value = cssutils.helper.normalize(self._value[0]) + + try: + val, dim = CSSPrimitiveValue._reNumDim.findall(value)[0] + except IndexError: + val, dim = value, u'' + try: + val = float(val) + if val == int(val): + val = int(val) + except ValueError: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: No float value %r' % self._value[0]) + + return val, dim + + def getFloatValue(self, unitType=None): + """(DOM) This method is used to get a float value in a + specified unit. If this CSS value doesn't contain a float value + or can't be converted into the specified unit, a DOMException + is raised. + + :param unitType: + to get the float value. The unit code can only be a float unit type + (i.e. CSS_NUMBER, CSS_PERCENTAGE, CSS_EMS, CSS_EXS, CSS_PX, CSS_CM, + CSS_MM, CSS_IN, CSS_PT, CSS_PC, CSS_DEG, CSS_RAD, CSS_GRAD, CSS_MS, + CSS_S, CSS_HZ, CSS_KHZ, CSS_DIMENSION) or None in which case + the current dimension is used. + + :returns: + not necessarily a float but some cases just an integer + e.g. if the value is ``1px`` it return ``1`` and **not** ``1.0`` + + Conversions might return strange values like 1.000000000001 + """ + if unitType is not None and unitType not in self._floattypes: + raise xml.dom.InvalidAccessErr( + u'unitType Parameter is not a float type') + + val, dim = self._getNumDim() + + if unitType is not None and self.primitiveType != unitType: + # convert if needed + try: + val = self._converter[self.primitiveType, unitType](val) + except KeyError: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r' + % (self.primitiveTypeString, + self._getCSSPrimitiveTypeString(unitType))) + + if val == int(val): + val = int(val) + + return val + + def setFloatValue(self, unitType, floatValue): + """(DOM) A method to set the float value with a specified unit. + If the property attached with this value can not accept the + specified unit or the float value, the value will be unchanged and + a DOMException will be raised. + + :param unitType: + a unit code as defined above. The unit code can only be a float + unit type + :param floatValue: + the new float value which does not have to be a float value but + may simple be an int e.g. if setting:: + + setFloatValue(CSS_PX, 1) + + :exceptions: + - :exc:`~xml.dom.InvalidAccessErr`: + Raised if the attached property doesn't + support the float value or the unit type. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this property is readonly. + """ + self._checkReadonly() + if unitType not in self._floattypes: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: unitType %r is not a float type' % + self._getCSSPrimitiveTypeString(unitType)) + try: + val = float(floatValue) + except ValueError, e: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: floatValue %r is not a float' % + floatValue) + + oldval, dim = self._getNumDim() + if self.primitiveType != unitType: + # convert if possible + try: + val = self._converter[unitType, self.primitiveType](val) + except KeyError: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r' + % (self.primitiveTypeString, + self._getCSSPrimitiveTypeString(unitType))) + + if val == int(val): + val = int(val) + + self.cssText = '%s%s' % (val, dim) + + def getStringValue(self): + """(DOM) This method is used to get the string value. If the + CSS value doesn't contain a string value, a DOMException is raised. + + Some properties (like 'font-family' or 'voice-family') + convert a whitespace separated list of idents to a string. + + Only the actual value is returned so e.g. all the following return the + actual value ``a``: url(a), attr(a), "a", 'a' + """ + if self.primitiveType not in self._stringtypes: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue %r is not a string type' + % self.primitiveTypeString) + + if CSSPrimitiveValue.CSS_ATTR == self.primitiveType: + return self._value[0].cssText[5:-1] + else: + return self._value[0] + + def setStringValue(self, stringType, stringValue): + """(DOM) A method to set the string value with the specified + unit. If the property attached to this value can't accept the + specified unit or the string value, the value will be unchanged and + a DOMException will be raised. + + :param stringType: + a string code as defined above. The string code can only be a + string unit type (i.e. CSS_STRING, CSS_URI, CSS_IDENT, and + CSS_ATTR). + :param stringValue: + the new string value + Only the actual value is expected so for (CSS_URI, "a") the + new value will be ``url(a)``. For (CSS_STRING, "'a'") + the new value will be ``"\\'a\\'"`` as the surrounding ``'`` are + not part of the string value + + :exceptions: + - :exc:`~xml.dom.InvalidAccessErr`: + Raised if the CSS value doesn't contain a + string value or if the string value can't be converted into + the specified unit. + + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this property is readonly. + """ + self._checkReadonly() + # self not stringType + if self.primitiveType not in self._stringtypes: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue %r is not a string type' + % self.primitiveTypeString) + # given stringType is no StringType + if stringType not in self._stringtypes: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: stringType %s is not a string type' + % self._getCSSPrimitiveTypeString(stringType)) + + if self._primitiveType != stringType: + raise xml.dom.InvalidAccessErr( + u'CSSPrimitiveValue: Cannot coerce primitiveType %r to %r' + % (self.primitiveTypeString, + self._getCSSPrimitiveTypeString(stringType))) + + if CSSPrimitiveValue.CSS_STRING == self._primitiveType: + self.cssText = cssutils.helper.string(stringValue) + elif CSSPrimitiveValue.CSS_URI == self._primitiveType: + self.cssText = cssutils.helper.uri(stringValue) + elif CSSPrimitiveValue.CSS_ATTR == self._primitiveType: + self.cssText = u'attr(%s)' % stringValue + else: + self.cssText = stringValue + self._primitiveType = stringType + + def getCounterValue(self): + """(DOM) This method is used to get the Counter value. If + this CSS value doesn't contain a counter value, a DOMException + is raised. Modification to the corresponding style property + can be achieved using the Counter interface. + + **Not implemented.** + """ + if not self.CSS_COUNTER == self.primitiveType: + raise xml.dom.InvalidAccessErr(u'Value is not a counter type') + # TODO: use Counter class + raise NotImplementedError() + + def getRGBColorValue(self): + """(DOM) This method is used to get the RGB color. If this + CSS value doesn't contain a RGB color value, a DOMException + is raised. Modification to the corresponding style property + can be achieved using the RGBColor interface. + """ + if self.primitiveType not in self._rbgtypes: + raise xml.dom.InvalidAccessErr(u'Value is not a RGBColor value') + return RGBColor(self._value[0]) + + def getRectValue(self): + """(DOM) This method is used to get the Rect value. If this CSS + value doesn't contain a rect value, a DOMException is raised. + Modification to the corresponding style property can be achieved + using the Rect interface. + + **Not implemented.** + """ + if self.primitiveType not in self._recttypes: + raise xml.dom.InvalidAccessErr(u'value is not a Rect value') + # TODO: use Rect class + raise NotImplementedError() + + def _getCssText(self): + """Overwrites CSSValue.""" + return cssutils.ser.do_css_CSSPrimitiveValue(self) + + def _setCssText(self, cssText): + """Use CSSValue.""" + return super(CSSPrimitiveValue, self)._setCssText(cssText) + + cssText = property(_getCssText, _setCssText, + doc="A string representation of the current value.") + + +class CSSValueList(CSSValue): + """The CSSValueList interface provides the abstraction of an ordered + collection of CSS values. + + Some properties allow an empty list into their syntax. In that case, + these properties take the none identifier. So, an empty list means + that the property has the value none. + + The items in the CSSValueList are accessible via an integral index, + starting from 0. + """ + cssValueType = CSSValue.CSS_VALUE_LIST + + def __init__(self, cssText=None, parent=None, readonly=False): + """Init a new CSSValueList""" + super(CSSValueList, self).__init__(cssText=cssText, + parent=parent, + readonly=readonly) + self._items = [] + + def __iter__(self): + "CSSValueList is iterable." + for item in self.__items(): + yield item.value + + def __str__(self): + return u"" % (self.__class__.__name__, + self.cssValueTypeString, + self.cssText, + self.length, + id(self)) + + def __items(self): + return [item for item in self._seq + if isinstance(item.value, CSSValue)] + + def item(self, index): + """(DOM) Retrieve a CSSValue by ordinal `index`. The + order in this collection represents the order of the values in the + CSS style property. If `index` is greater than or equal to the number + of values in the list, this returns ``None``. + """ + try: + return self.__items()[index].value + except IndexError: + return None + + length = property(lambda self: len(self.__items()), + doc=u"(DOM attribute) The number of CSSValues in the " + u"list.") + + +class CSSFunction(CSSPrimitiveValue): + """A CSS function value like rect() etc.""" + _functionName = u'CSSFunction' + primitiveType = CSSPrimitiveValue.CSS_UNKNOWN + + def __init__(self, cssText=None, parent=None, readonly=False): + """ + Init a new CSSFunction + + :param cssText: + the parsable cssText of the value + :param readonly: + defaults to False + """ + super(CSSFunction, self).__init__(parent=parent) + self._funcType = None + self.valid = False + self.wellformed = False + if cssText is not None: + self.cssText = cssText + self._readonly = readonly + + def _productiondefinition(self): + """Return definition used for parsing.""" + types = self._prods # rename! + + value = Sequence(PreDef.unary(), + Prod(name='PrimitiveValue', + match=lambda t, v: t in (types.DIMENSION, + types.HASH, + types.IDENT, + types.NUMBER, + types.PERCENTAGE, + types.STRING), + toSeq=lambda t, tokens: (t[0], + CSSPrimitiveValue(t[1])) + ) + ) + valueOrFunc = Choice(value, + # FUNC is actually not in spec but used in e.g. Prince + PreDef.function(toSeq=lambda t, + tokens: ('FUNCTION', + CSSFunction( + cssutils.helper.pushtoken(t, tokens)) + ) + ) + ) + funcProds = Sequence(Prod(name='FUNC', + match=lambda t, v: t == types.FUNCTION, + toSeq=lambda t, tokens: (t[0], cssutils.helper.normalize(t[1]))), + Choice(Sequence(valueOrFunc, + # more values starting with Comma + # should use store where colorType is saved to + # define min and may, closure? + Sequence(PreDef.comma(), + valueOrFunc, + minmax=lambda: (0, None)), + PreDef.funcEnd(stop=True)), + PreDef.funcEnd(stop=True)) + ) + return funcProds + + def _setCssText(self, cssText): + self._checkReadonly() + # store: colorType, parts + wellformed, seq, store, unusedtokens = ProdParser().parse(cssText, + self._functionName, + self._productiondefinition(), + keepS=True) + if wellformed: + # combine +/- and following CSSPrimitiveValue, remove S + newseq = self._tempSeq() + i, end = 0, len(seq) + while i < end: + item = seq[i] + if item.type == self._prods.S: + pass + elif item.value == u'+' or item.value == u'-': + i += 1 + next = seq[i] + newval = next.value + if isinstance(newval, CSSPrimitiveValue): + newval.setFloatValue(newval.primitiveType, + float(item.value + str(newval.getFloatValue()))) + newseq.append(newval, next.type, + item.line, item.col) + else: + # expressions only? + newseq.appendItem(item) + newseq.appendItem(next) + else: + newseq.appendItem(item) + + i += 1 + + self.wellformed = True + self._setSeq(newseq) + self._funcType = newseq[0].value + + cssText = property(lambda self: cssutils.ser.do_css_FunctionValue(self), + _setCssText) + + funcType = property(lambda self: self._funcType) + + +class RGBColor(CSSFunction): + """A CSS color like RGB, RGBA or a simple value like `#000` or `red`.""" + + _functionName = u'Function rgb()' + + def __init__(self, cssText=None, parent=None, readonly=False): + """ + Init a new RGBColor + + :param cssText: + the parsable cssText of the value + :param readonly: + defaults to False + """ + super(CSSFunction, self).__init__(parent=parent) + self._colorType = None + self.valid = False + self.wellformed = False + if cssText is not None: + try: + # if it is a Function object + cssText = cssText.cssText + except AttributeError: + pass + self.cssText = cssText + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.colorType, + self.cssText, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + types = self._prods # rename! + valueProd = Prod(name='value', + match=lambda t, v: t in (types.NUMBER, types.PERCENTAGE), + toSeq=lambda t, v: (CSSPrimitiveValue, CSSPrimitiveValue(v)), + toStore='parts' + ) + # COLOR PRODUCTION + funccolor = Sequence(Prod(name='FUNC', + match=lambda t, v: t == types.FUNCTION and cssutils.helper.normalize(v) in ('rgb(', 'rgba(', 'hsl(', 'hsla('), + toSeq=lambda t, v: (t, v),#cssutils.helper.normalize(v)), + toStore='colorType'), + PreDef.unary(), + valueProd, + # 2 or 3 more values starting with Comma + Sequence(PreDef.comma(), + PreDef.unary(), + valueProd, + minmax=lambda: (2, 3)), + PreDef.funcEnd() + ) + colorprods = Choice(funccolor, + PreDef.hexcolor('colorType'), + Prod(name='named color', + match=lambda t, v: t == types.IDENT, + toStore='colorType' + ) + ) + # store: colorType, parts + wellformed, seq, store, unusedtokens = ProdParser().parse(cssText, + u'RGBColor', + colorprods, + keepS=True, + store={'parts': []}) + + if wellformed: + self.wellformed = True + if store['colorType'].type == self._prods.HASH: + self._colorType = 'HEX' + elif store['colorType'].type == self._prods.IDENT: + self._colorType = 'Named Color' + else: + self._colorType = store['colorType'].value[:-1] + #self._colorType = cssutils.helper.normalize(store['colorType'].value)[:-1] + + self._setSeq(seq) + + cssText = property(lambda self: cssutils.ser.do_css_RGBColor(self), + _setCssText) + + colorType = property(lambda self: self._colorType) + + +class CalcValue(CSSFunction): + """Calc Function""" + _functionName = u'Function calc()' + + def _productiondefinition(self): + """Return defintion used for parsing.""" + types = self._prods # rename! + + def toSeq(t, tokens): + "Do not normalize function name!" + return t[0], t[1] + + funcProds = Sequence(Prod(name='calc', + match=lambda t, v: t == types.FUNCTION, + toSeq=toSeq + ), + Sequence(Choice(Prod(name='nested function', + match=lambda t, v: t == self._prods.FUNCTION, + toSeq=lambda t, tokens: (CSSFunction._functionName, + CSSFunction(cssutils.helper.pushtoken(t, + tokens))) + ), + Prod(name='part', + match=lambda t, v: v != u')', + toSeq=lambda t, tokens: (t[0], t[1])), + ), + minmax=lambda: (0, None)), + PreDef.funcEnd(stop=True)) + return funcProds + + def _getCssText(self): + return cssutils.ser.do_css_CalcValue(self) + + def _setCssText(self, cssText): + return super(CalcValue, self)._setCssText(cssText) + + cssText = property(_getCssText, _setCssText, + doc=u"A string representation of the current value.") + + +class ExpressionValue(CSSFunction): + """Special IE only CSSFunction which may contain *anything*. + Used for expressions and ``alpha(opacity=100)`` currently.""" + _functionName = u'Expression (IE only)' + + def _productiondefinition(self): + """Return defintion used for parsing.""" + types = self._prods # rename! + + def toSeq(t, tokens): + "Do not normalize function name!" + return t[0], t[1] + + funcProds = Sequence(Prod(name='expression', + match=lambda t, v: t == types.FUNCTION, + toSeq=toSeq + ), + Sequence(Choice(Prod(name='nested function', + match=lambda t, v: t == self._prods.FUNCTION, + toSeq=lambda t, tokens: (ExpressionValue._functionName, + ExpressionValue(cssutils.helper.pushtoken(t, + tokens))) + ), + Prod(name='part', + match=lambda t, v: v != u')', + toSeq=lambda t, tokens: (t[0], t[1])), + ), + minmax=lambda: (0, None)), + PreDef.funcEnd(stop=True)) + return funcProds + + def _getCssText(self): + return cssutils.ser.do_css_ExpressionValue(self) + + def _setCssText(self, cssText): + #self._log.warn(u'CSSValue: Unoffial and probably invalid MS value used!') + return super(ExpressionValue, self)._setCssText(cssText) + + cssText = property(_getCssText, _setCssText, + doc=u"A string representation of the current value.") + + +class CSSVariable(CSSValue): + """The CSSVariable represents a call to CSS Variable.""" + + def __init__(self, cssText=None, parent=None, readonly=False): + """Init a new CSSVariable. + + :param cssText: + the parsable cssText of the value, e.g. ``var(x)`` + :param readonly: + defaults to False + """ + self._name = None + super(CSSVariable, self).__init__(cssText=cssText, + parent=parent, + readonly=readonly) + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.name, + self.value, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + types = self._prods # rename! + + funcProds = Sequence(Prod(name='var', + match=lambda t, v: t == types.FUNCTION + ), + PreDef.ident(toStore='ident'), + PreDef.funcEnd(stop=True)) + + # store: name of variable + store = {'ident': None} + wellformed, seq, store, unusedtokens = ProdParser().parse(cssText, + u'CSSVariable', + funcProds, + keepS=True) + if wellformed: + self._name = store['ident'].value + self._setSeq(seq) + self.wellformed = True + + cssText = property(lambda self: cssutils.ser.do_css_CSSVariable(self), + _setCssText, + doc=u"A string representation of the current variable.") + + cssValueType = CSSValue.CSS_VARIABLE + + # TODO: writable? check if var (value) available? + name = property(lambda self: self._name) + + def _getValue(self): + "Find contained sheet and @variables there" + try: + variables = self.parent.parent.parentRule.parentStyleSheet.variables + except AttributeError: + return None + else: + try: + return variables[self.name] + except KeyError: + return None + + value = property(_getValue) diff --git a/libs/cssutils/css/cssvariablesdeclaration.py b/libs/cssutils/css/cssvariablesdeclaration.py new file mode 100755 index 00000000..2b33f670 --- /dev/null +++ b/libs/cssutils/css/cssvariablesdeclaration.py @@ -0,0 +1,330 @@ +"""CSSVariablesDeclaration +http://disruptive-innovations.com/zoo/cssvariables/#mozTocId496530 +""" +__all__ = ['CSSVariablesDeclaration'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: cssstyledeclaration.py 1819 2009-08-01 20:52:43Z cthedot $' + +from cssutils.prodparser import * +from cssutils.helper import normalize +from value import PropertyValue +import cssutils +import itertools +import xml.dom + +class CSSVariablesDeclaration(cssutils.util._NewBase): + """The CSSVariablesDeclaration interface represents a single block of + variable declarations. + """ + def __init__(self, cssText=u'', parentRule=None, readonly=False): + """ + :param cssText: + Shortcut, sets CSSVariablesDeclaration.cssText + :param parentRule: + The CSS rule that contains this declaration block or + None if this CSSVariablesDeclaration is not attached to a CSSRule. + :param readonly: + defaults to False + + Format:: + + variableset + : vardeclaration [ ';' S* vardeclaration ]* S* + ; + + vardeclaration + : varname ':' S* term + ; + + varname + : IDENT S* + ; + """ + super(CSSVariablesDeclaration, self).__init__() + self._parentRule = parentRule + self._vars = {} + if cssText: + self.cssText = cssText + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(cssText=%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % ( + self.__class__.__name__, + self.length, + id(self)) + + def __contains__(self, variableName): + """Check if a variable is in variable declaration block. + + :param variableName: + a string + """ + return normalize(variableName) in self.keys() + + def __getitem__(self, variableName): + """Retrieve the value of variable ``variableName`` from this + declaration. + """ + return self.getVariableValue(variableName) + + def __setitem__(self, variableName, value): + self.setVariable(variableName, value) + + def __delitem__(self, variableName): + return self.removeVariable(variableName) + + def __iter__(self): + """Iterator of names of set variables.""" + for name in self.keys(): + yield name + + def keys(self): + """Analoguous to standard dict returns variable names which are set in + this declaration.""" + return self._vars.keys() + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_css_CSSVariablesDeclaration(self) + + def _setCssText(self, cssText): + """Setting this attribute will result in the parsing of the new value + and resetting of all the properties in the declaration block + including the removal or addition of properties. + + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or a property is readonly. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + + Format:: + + variableset + : vardeclaration [ ';' S* vardeclaration ]* + ; + + vardeclaration + : varname ':' S* term + ; + + varname + : IDENT S* + ; + + expr + : [ VARCALL | term ] [ operator [ VARCALL | term ] ]* + ; + + """ + self._checkReadonly() + + vardeclaration = Sequence( + PreDef.ident(), + PreDef.char(u':', u':', toSeq=False), + #PreDef.S(toSeq=False, optional=True), + Prod(name=u'term', match=lambda t, v: True, + toSeq=lambda t, tokens: (u'value', + PropertyValue(itertools.chain([t], + tokens), + parent=self) + ) + ) + ) + prods = Sequence(vardeclaration, + Sequence(PreDef.S(optional=True), + PreDef.char(u';', u';', toSeq=False), + PreDef.S(optional=True), + vardeclaration, + minmax=lambda: (0, None)), + PreDef.S(optional=True), + PreDef.char(u';', u';', toSeq=False, optional=True) + ) + # parse + wellformed, seq, store, notused = \ + ProdParser().parse(cssText, + u'CSSVariableDeclaration', + prods) + if wellformed: + newseq = self._tempSeq() + newvars = {} + + # seq contains only name: value pairs plus comments etc + nameitem = None + for item in seq: + if u'IDENT' == item.type: + nameitem = item + elif u'value' == item.type: + nname = normalize(nameitem.value) + if nname in newvars: + # replace var with same name + for i, it in enumerate(newseq): + if normalize(it.value[0]) == nname: + newseq.replace(i, + (nameitem.value, item.value), + 'var', + nameitem.line, nameitem.col) + else: + # saved non normalized name for reserialization + newseq.append((nameitem.value, item.value), + 'var', + nameitem.line, nameitem.col) + +# newseq.append((nameitem.value, item.value), +# 'var', +# nameitem.line, nameitem.col) + + newvars[nname] = item.value + + else: + newseq.appendItem(item) + + self._setSeq(newseq) + self._vars = newvars + self.wellformed = True + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) A parsable textual representation of the declaration " + u"block excluding the surrounding curly braces.") + + def _setParentRule(self, parentRule): + self._parentRule = parentRule + + parentRule = property(lambda self: self._parentRule, _setParentRule, + doc=u"(DOM) The CSS rule that contains this" + u" declaration block or None if this block" + u" is not attached to a CSSRule.") + + def getVariableValue(self, variableName): + """Used to retrieve the value of a variable if it has been explicitly + set within this variable declaration block. + + :param variableName: + The name of the variable. + :returns: + the value of the variable if it has been explicitly set in this + variable declaration block. Returns the empty string if the + variable has not been set. + """ + try: + return self._vars[normalize(variableName)].cssText + except KeyError, e: + return u'' + + def removeVariable(self, variableName): + """Used to remove a variable if it has been explicitly set within this + variable declaration block. + + :param variableName: + The name of the variable. + :returns: + the value of the variable if it has been explicitly set for this + variable declaration block. Returns the empty string if the + variable has not been set. + + :exceptions: + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly is readonly. + """ + normalname = variableName + try: + r = self._vars[normalname] + except KeyError, e: + return u'' + else: + self.seq._readonly = False + if normalname in self._vars: + for i, x in enumerate(self.seq): + if x.value[0] == variableName: + del self.seq[i] + self.seq._readonly = True + del self._vars[normalname] + + return r.cssText + + def setVariable(self, variableName, value): + """Used to set a variable value within this variable declaration block. + + :param variableName: + The name of the CSS variable. + :param value: + The new value of the variable, may also be a PropertyValue object. + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified value has a syntax error and is + unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this declaration is readonly or the property is + readonly. + """ + self._checkReadonly() + + # check name + wellformed, seq, store, unused = \ + ProdParser().parse(normalize(variableName), + u'variableName', + Sequence(PreDef.ident())) + if not wellformed: + self._log.error(u'Invalid variableName: %r: %r' + % (variableName, value)) + else: + # check value + if isinstance(value, PropertyValue): + v = value + else: + v = PropertyValue(cssText=value, parent=self) + + if not v.wellformed: + self._log.error(u'Invalid variable value: %r: %r' + % (variableName, value)) + else: + # update seq + self.seq._readonly = False + + variableName = normalize(variableName) + + if variableName in self._vars: + for i, x in enumerate(self.seq): + if x.value[0] == variableName: + self.seq.replace(i, + [variableName, v], + x.type, + x.line, + x.col) + break + else: + self.seq.append([variableName, v], 'var') + self.seq._readonly = True + self._vars[variableName] = v + + def item(self, index): + """Used to retrieve the variables that have been explicitly set in + this variable declaration block. The order of the variables + retrieved using this method does not have to be the order in which + they were set. This method can be used to iterate over all variables + in this variable declaration block. + + :param index: + of the variable name to retrieve, negative values behave like + negative indexes on Python lists, so -1 is the last element + + :returns: + The name of the variable at this ordinal position. The empty + string if no variable exists at this position. + """ + try: + return self.keys()[index] + except IndexError: + return u'' + + length = property(lambda self: len(self._vars), + doc=u"The number of variables that have been explicitly set in this" + u" variable declaration block. The range of valid indices is 0" + u" to length-1 inclusive.") diff --git a/libs/cssutils/css/cssvariablesrule.py b/libs/cssutils/css/cssvariablesrule.py new file mode 100755 index 00000000..ce5335bf --- /dev/null +++ b/libs/cssutils/css/cssvariablesrule.py @@ -0,0 +1,198 @@ +"""CSSVariables implements (and only partly) experimental +`CSS Variables `_ +""" +__all__ = ['CSSVariablesRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: cssfontfacerule.py 1818 2009-07-30 21:39:00Z cthedot $' + +from cssvariablesdeclaration import CSSVariablesDeclaration +import cssrule +import cssutils +import xml.dom + +class CSSVariablesRule(cssrule.CSSRule): + """ + The CSSVariablesRule interface represents a @variables rule within a CSS + style sheet. The @variables rule is used to specify variables. + + cssutils uses a :class:`~cssutils.css.CSSVariablesDeclaration` to + represent the variables. + + Format:: + + variables + VARIABLES_SYM S* medium [ COMMA S* medium ]* LBRACE S* + variableset* '}' S* + ; + + for variableset see :class:`cssutils.css.CSSVariablesDeclaration` + + **Media are not implemented. Reason is that cssutils is using CSS + variables in a kind of preprocessing and therefor no media information + is available at this stage. For now do not use media!** + + Example:: + + @variables { + CorporateLogoBGColor: #fe8d12; + } + + div.logoContainer { + background-color: var(CorporateLogoBGColor); + } + """ + def __init__(self, mediaText=None, variables=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + If readonly allows setting of properties in constructor only. + """ + super(CSSVariablesRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + self._atkeyword = u'@variables' + + # dummy + self._media = cssutils.stylesheets.MediaList(mediaText, + readonly=readonly) + + if variables: + self.variables = variables + else: + self.variables = CSSVariablesDeclaration(parentRule=self) + + self._readonly = readonly + + def __repr__(self): + return u"cssutils.css.%s(mediaText=%r, variables=%r)" % ( + self.__class__.__name__, + self._media.mediaText, + self.variables.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self._media.mediaText, + self.variables.cssText, + self.valid, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_CSSVariablesRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + + Format:: + + variables + : VARIABLES_SYM S* medium [ COMMA S* medium ]* LBRACE S* + variableset* '}' S* + ; + + variableset + : LBRACE S* vardeclaration [ ';' S* vardeclaration ]* '}' S* + ; + """ + super(CSSVariablesRule, self)._setCssText(cssText) + + tokenizer = self._tokenize2(cssText) + attoken = self._nexttoken(tokenizer, None) + if self._type(attoken) != self._prods.VARIABLES_SYM: + self._log.error(u'CSSVariablesRule: No CSSVariablesRule found: %s' % + self._valuestr(cssText), + error=xml.dom.InvalidModificationErr) + else: + newVariables = CSSVariablesDeclaration(parentRule=self) + ok = True + + beforetokens, brace = self._tokensupto2(tokenizer, + blockstartonly=True, + separateEnd=True) + if self._tokenvalue(brace) != u'{': + ok = False + self._log.error(u'CSSVariablesRule: No start { of variable ' + u'declaration found: %r' + % self._valuestr(cssText), brace) + + # parse stuff before { which should be comments and S only + new = {'wellformed': True} + newseq = self._tempSeq()#[] + + beforewellformed, expected = self._parse(expected=':', + seq=newseq, tokenizer=self._tokenize2(beforetokens), + productions={}) + ok = ok and beforewellformed and new['wellformed'] + + variablestokens, braceorEOFtoken = self._tokensupto2(tokenizer, + blockendonly=True, + separateEnd=True) + + val, type_ = self._tokenvalue(braceorEOFtoken), \ + self._type(braceorEOFtoken) + if val != u'}' and type_ != 'EOF': + ok = False + self._log.error(u'CSSVariablesRule: No "}" after variables ' + u'declaration found: %r' + % self._valuestr(cssText)) + + nonetoken = self._nexttoken(tokenizer) + if nonetoken: + ok = False + self._log.error(u'CSSVariablesRule: Trailing content found.', + token=nonetoken) + + if 'EOF' == type_: + # add again as variables needs it + variablestokens.append(braceorEOFtoken) + # SET but may raise: + newVariables.cssText = variablestokens + + if ok: + # contains probably comments only upto { + self._setSeq(newseq) + self.variables = newVariables + + cssText = property(_getCssText, _setCssText, + doc=u"(DOM) The parsable textual representation of this " + u"rule.") + + media = property(doc=u"NOT IMPLEMENTED! As cssutils resolves variables "\ + u"during serializing media information is lost.") + + def _setVariables(self, variables): + """ + :param variables: + a CSSVariablesDeclaration or string + """ + self._checkReadonly() + if isinstance(variables, basestring): + self._variables = CSSVariablesDeclaration(cssText=variables, + parentRule=self) + else: + variables._parentRule = self + self._variables = variables + + variables = property(lambda self: self._variables, _setVariables, + doc=u"(DOM) The variables of this rule set, a " + u":class:`cssutils.css.CSSVariablesDeclaration`.") + + type = property(lambda self: self.VARIABLES_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + valid = property(lambda self: True, doc='NOT IMPLEMTED REALLY (TODO)') + + # constant but needed: + wellformed = property(lambda self: True) diff --git a/libs/cssutils/css/marginrule.py b/libs/cssutils/css/marginrule.py new file mode 100755 index 00000000..0c789fad --- /dev/null +++ b/libs/cssutils/css/marginrule.py @@ -0,0 +1,215 @@ +"""MarginRule implements DOM Level 2 CSS MarginRule.""" +__all__ = ['MarginRule'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.prodparser import * +from cssstyledeclaration import CSSStyleDeclaration +import cssrule +import cssutils +import xml.dom + +class MarginRule(cssrule.CSSRule): + """ + A margin at-rule consists of an ATKEYWORD that identifies the margin box + (e.g. '@top-left') and a block of declarations (said to be in the margin + context). + + Format:: + + margin : + margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* + ; + + margin_sym : + TOPLEFTCORNER_SYM | + TOPLEFT_SYM | + TOPCENTER_SYM | + TOPRIGHT_SYM | + TOPRIGHTCORNER_SYM | + BOTTOMLEFTCORNER_SYM | + BOTTOMLEFT_SYM | + BOTTOMCENTER_SYM | + BOTTOMRIGHT_SYM | + BOTTOMRIGHTCORNER_SYM | + LEFTTOP_SYM | + LEFTMIDDLE_SYM | + LEFTBOTTOM_SYM | + RIGHTTOP_SYM | + RIGHTMIDDLE_SYM | + RIGHTBOTTOM_SYM + ; + + e.g.:: + + @top-left { + content: "123"; + } + """ + margins = ['@top-left-corner', + '@top-left', + '@top-center', + '@top-right', + '@top-right-corner', + '@bottom-left-corner', + '@bottom-left', + '@bottom-center', + '@bottom-right', + '@bottom-right-corner', + '@left-top', + '@left-middle', + '@left-bottom', + '@right-top', + '@right-middle', + '@right-bottom' + ] + + def __init__(self, margin=None, style=None, parentRule=None, + parentStyleSheet=None, readonly=False): + """ + :param atkeyword: + The margin area, e.g. '@top-left' for this rule + :param style: + CSSStyleDeclaration for this MarginRule + """ + super(MarginRule, self).__init__(parentRule=parentRule, + parentStyleSheet=parentStyleSheet) + + self._atkeyword = self._keyword = None + + if margin: + self.margin = margin + + if style: + self.style = style + else: + self.style = CSSStyleDeclaration(parentRule=self) + + self._readonly = readonly + + def _setMargin(self, margin): + """Check if new keyword fits the rule it is used for.""" + n = self._normalize(margin) + + if n not in MarginRule.margins: + self._log.error(u'Invalid margin @keyword for this %s rule: %r' % + (self.margin, margin), + error=xml.dom.InvalidModificationErr) + + else: + self._atkeyword = n + self._keyword = margin + + margin = property(lambda self: self._atkeyword, _setMargin, + doc=u"Margin area of parent CSSPageRule. " + u"`margin` and `atkeyword` are both normalized " + u"@keyword of the @rule.") + + atkeyword = margin + + def __repr__(self): + return u"cssutils.css.%s(margin=%r, style=%r)" % (self.__class__.__name__, + self.margin, + self.style.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.margin, + self.style.cssText, + id(self)) + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_MarginRule(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + Raised if the specified CSS string value represents a different + type of rule than the current one. + - :exc:`~xml.dom.HierarchyRequestErr`: + Raised if the rule cannot be inserted at this point in the + style sheet. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + super(MarginRule, self)._setCssText(cssText) + + # TEMP: all style tokens are saved in store to fill styledeclaration + # TODO: resolve when all generators + styletokens = Prod(name='styletokens', + match=lambda t, v: v != u'}', + #toSeq=False, + toStore='styletokens', + storeToken=True + ) + + prods = Sequence(Prod(name='@ margin', + match=lambda t, v: + t == 'ATKEYWORD' and + self._normalize(v) in MarginRule.margins, + toStore='margin' + # TODO? + #, exception=xml.dom.InvalidModificationErr + ), + PreDef.char('OPEN', u'{'), + Sequence(Choice(PreDef.unknownrule(toStore='@'), + styletokens), + minmax=lambda: (0, None) + ), + PreDef.char('CLOSE', u'}', stopAndKeep=True) + ) + # parse + ok, seq, store, unused = ProdParser().parse(cssText, + u'MarginRule', + prods) + + if ok: + # TODO: use seq for serializing instead of fixed stuff? + self._setSeq(seq) + + if 'margin' in store: + # may raise: + self.margin = store['margin'].value + else: + self._log.error(u'No margin @keyword for this %s rule' % + self.margin, + error=xml.dom.InvalidModificationErr) + + # new empty style + self.style = CSSStyleDeclaration(parentRule=self) + + if 'styletokens' in store: + # may raise: + self.style.cssText = store['styletokens'] + + + cssText = property(fget=_getCssText, fset=_setCssText, + doc=u"(DOM) The parsable textual representation.") + + def _setStyle(self, style): + """ + :param style: A string or CSSStyleDeclaration which replaces the + current style object. + """ + self._checkReadonly() + if isinstance(style, basestring): + self._style = CSSStyleDeclaration(cssText=style, parentRule=self) + else: + style._parentRule = self + self._style = style + + style = property(lambda self: self._style, _setStyle, + doc=u"(DOM) The declaration-block of this rule set.") + + type = property(lambda self: self.MARGIN_RULE, + doc=u"The type of this rule, as defined by a CSSRule " + u"type constant.") + + wellformed = property(lambda self: bool(self.atkeyword)) + \ No newline at end of file diff --git a/libs/cssutils/css/property.py b/libs/cssutils/css/property.py new file mode 100755 index 00000000..fad240c0 --- /dev/null +++ b/libs/cssutils/css/property.py @@ -0,0 +1,510 @@ +"""Property is a single CSS property in a CSSStyleDeclaration.""" +__all__ = ['Property'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.helper import Deprecated +from value import PropertyValue +import cssutils +import xml.dom + +class Property(cssutils.util.Base): + """A CSS property in a StyleDeclaration of a CSSStyleRule (cssutils). + + Format:: + + property = name + : IDENT S* + ; + + expr = value + : term [ operator term ]* + ; + term + : unary_operator? + [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | + ANGLE S* | TIME S* | FREQ S* | function ] + | STRING S* | IDENT S* | URI S* | hexcolor + ; + function + : FUNCTION S* expr ')' S* + ; + /* + * There is a constraint on the color that it must + * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) + * after the "#"; e.g., "#000" is OK, but "#abcd" is not. + */ + hexcolor + : HASH S* + ; + + prio + : IMPORTANT_SYM S* + ; + + """ + def __init__(self, name=None, value=None, priority=u'', + _mediaQuery=False, parent=None): + """ + :param name: + a property name string (will be normalized) + :param value: + a property value string + :param priority: + an optional priority string which currently must be u'', + u'!important' or u'important' + :param _mediaQuery: + if ``True`` value is optional (used by MediaQuery) + :param parent: + the parent object, normally a + :class:`cssutils.css.CSSStyleDeclaration` + """ + super(Property, self).__init__() + self.seqs = [[], None, []] + self.wellformed = False + self._mediaQuery = _mediaQuery + self.parent = parent + + self.__nametoken = None + self._name = u'' + self._literalname = u'' + self.seqs[1] = PropertyValue(parent=self) + if name: + self.name = name + self.propertyValue = value + + self._priority = u'' + self._literalpriority = u'' + if priority: + self.priority = priority + + def __repr__(self): + return u"cssutils.css.%s(name=%r, value=%r, priority=%r)" % ( + self.__class__.__name__, + self.literalname, + self.propertyValue.cssText, + self.priority) + + def __str__(self): + return u"<%s.%s object name=%r value=%r priority=%r valid=%r at 0x%x>" \ + % (self.__class__.__module__, + self.__class__.__name__, + self.name, + self.propertyValue.cssText, + self.priority, + self.valid, + id(self)) + + def _isValidating(self): + """Return True if validation is enabled.""" + try: + return self.parent.validating + except AttributeError: + # default (no parent) + return True + + def _getCssText(self): + """Return serialized property cssText.""" + return cssutils.ser.do_Property(self) + + def _setCssText(self, cssText): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error and + is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if the rule is readonly. + """ + # check and prepare tokenlists for setting + tokenizer = self._tokenize2(cssText) + nametokens = self._tokensupto2(tokenizer, propertynameendonly=True) + if nametokens: + wellformed = True + + valuetokens = self._tokensupto2(tokenizer, + propertyvalueendonly=True) + prioritytokens = self._tokensupto2(tokenizer, + propertypriorityendonly=True) + + if self._mediaQuery and not valuetokens: + # MediaQuery may consist of name only + self.name = nametokens + self.propertyValue = None + self.priority = None + return + + # remove colon from nametokens + colontoken = nametokens.pop() + if self._tokenvalue(colontoken) != u':': + wellformed = False + self._log.error(u'Property: No ":" after name found: %s' % + self._valuestr(cssText), colontoken) + elif not nametokens: + wellformed = False + self._log.error(u'Property: No property name found: %s' % + self._valuestr(cssText), colontoken) + + if valuetokens: + if self._tokenvalue(valuetokens[-1]) == u'!': + # priority given, move "!" to prioritytokens + prioritytokens.insert(0, valuetokens.pop(-1)) + else: + wellformed = False + self._log.error(u'Property: No property value found: %s' % + self._valuestr(cssText), colontoken) + + if wellformed: + self.wellformed = True + self.name = nametokens + self.propertyValue = valuetokens + self.priority = prioritytokens + + # also invalid values are set! + + if self._isValidating(): + self.validate() + + else: + self._log.error(u'Property: No property name found: %s' % + self._valuestr(cssText)) + + cssText = property(fget=_getCssText, fset=_setCssText, + doc="A parsable textual representation.") + + def _setName(self, name): + """ + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified name has a syntax error and is + unparsable. + """ + # for closures: must be a mutable + new = {'literalname': None, + 'wellformed': True} + + def _ident(expected, seq, token, tokenizer=None): + # name + if 'name' == expected: + new['literalname'] = self._tokenvalue(token).lower() + seq.append(new['literalname']) + return 'EOF' + else: + new['wellformed'] = False + self._log.error(u'Property: Unexpected ident.', token) + return expected + + newseq = [] + wellformed, expected = self._parse(expected='name', + seq=newseq, + tokenizer=self._tokenize2(name), + productions={'IDENT': _ident}) + wellformed = wellformed and new['wellformed'] + + # post conditions + # define a token for error logging + if isinstance(name, list): + token = name[0] + self.__nametoken = token + else: + token = None + + if not new['literalname']: + wellformed = False + self._log.error(u'Property: No name found: %s' % + self._valuestr(name), token=token) + + if wellformed: + self.wellformed = True + self._literalname = new['literalname'] + self._name = self._normalize(self._literalname) + self.seqs[0] = newseq + + # validate + if self._isValidating() and self._name not in cssutils.profile.knownNames: + # self.valid = False + self._log.warn(u'Property: Unknown Property name.', + token=token, neverraise=True) + else: + pass +# self.valid = True +# if self.propertyValue: +# self.propertyValue._propertyName = self._name +# #self.valid = self.propertyValue.valid + else: + self.wellformed = False + + name = property(lambda self: self._name, _setName, + doc="Name of this property.") + + literalname = property(lambda self: self._literalname, + doc="Readonly literal (not normalized) name " + "of this property") + + def _setPropertyValue(self, cssText): + """ + See css.PropertyValue + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + (according to the attached property) or is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + TODO: Raised if the specified CSS string value represents a different + type of values than the values allowed by the CSS property. + """ + if self._mediaQuery and not cssText: + self.seqs[1] = PropertyValue(parent=self) + else: + self.seqs[1].cssText = cssText + self.wellformed = self.wellformed and self.seqs[1].wellformed + + propertyValue = property(lambda self: self.seqs[1], + _setPropertyValue, + doc=u"(cssutils) PropertyValue object of property") + + + def _getValue(self): + if self.propertyValue: + # value without comments + return self.propertyValue.value + else: + return u'' + + def _setValue(self, value): + self._setPropertyValue(value) + + value = property(_getValue, _setValue, + doc="The textual value of this Properties propertyValue.") + + def _setPriority(self, priority): + """ + priority + a string, currently either u'', u'!important' or u'important' + + Format:: + + prio + : IMPORTANT_SYM S* + ; + + "!"{w}"important" {return IMPORTANT_SYM;} + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified priority has a syntax error and is + unparsable. + In this case a priority not equal to None, "" or "!{w}important". + As CSSOM defines CSSStyleDeclaration.getPropertyPriority resulting + in u'important' this value is also allowed to set a Properties + priority + """ + if self._mediaQuery: + self._priority = u'' + self._literalpriority = u'' + if priority: + self._log.error(u'Property: No priority in a MediaQuery - ' + u'ignored.') + return + + if isinstance(priority, basestring) and\ + u'important' == self._normalize(priority): + priority = u'!%s' % priority + + # for closures: must be a mutable + new = {'literalpriority': u'', + 'wellformed': True} + + def _char(expected, seq, token, tokenizer=None): + # "!" + val = self._tokenvalue(token) + if u'!' == expected == val: + seq.append(val) + return 'important' + else: + new['wellformed'] = False + self._log.error(u'Property: Unexpected char.', token) + return expected + + def _ident(expected, seq, token, tokenizer=None): + # "important" + val = self._tokenvalue(token) + if 'important' == expected: + new['literalpriority'] = val + seq.append(val) + return 'EOF' + else: + new['wellformed'] = False + self._log.error(u'Property: Unexpected ident.', token) + return expected + + newseq = [] + wellformed, expected = self._parse(expected='!', + seq=newseq, + tokenizer=self._tokenize2(priority), + productions={'CHAR': _char, + 'IDENT': _ident}) + wellformed = wellformed and new['wellformed'] + + # post conditions + if priority and not new['literalpriority']: + wellformed = False + self._log.info(u'Property: Invalid priority: %s' % + self._valuestr(priority)) + + if wellformed: + self.wellformed = self.wellformed and wellformed + self._literalpriority = new['literalpriority'] + self._priority = self._normalize(self.literalpriority) + self.seqs[2] = newseq + # validate priority + if self._priority not in (u'', u'important'): + self._log.error(u'Property: No CSS priority value: %s' % + self._priority) + + priority = property(lambda self: self._priority, _setPriority, + doc="Priority of this property.") + + literalpriority = property(lambda self: self._literalpriority, + doc="Readonly literal (not normalized) priority of this property") + + def _setParent(self, parent): + self._parent = parent + + parent = property(lambda self: self._parent, _setParent, + doc="The Parent Node (normally a CSSStyledeclaration) of this " + "Property") + + def validate(self): + """Validate value against `profiles` which are checked dynamically. + properties in e.g. @font-face rules are checked against + ``cssutils.profile.CSS3_FONT_FACE`` only. + + For each of the following cases a message is reported: + + - INVALID (so the property is known but not valid) + ``ERROR Property: Invalid value for "{PROFILE-1[/PROFILE-2...]" + property: ...`` + + - VALID but not in given profiles or defaultProfiles + ``WARNING Property: Not valid for profile "{PROFILE-X}" but valid + "{PROFILE-Y}" property: ...`` + + - VALID in current profile + ``DEBUG Found valid "{PROFILE-1[/PROFILE-2...]" property...`` + + - UNKNOWN property + ``WARNING Unknown Property name...`` is issued + + so for example:: + + cssutils.log.setLevel(logging.DEBUG) + parser = cssutils.CSSParser() + s = parser.parseString('''body { + unknown-property: x; + color: 4; + color: rgba(1,2,3,4); + color: red + }''') + + # Log output: + + WARNING Property: Unknown Property name. [2:9: unknown-property] + ERROR Property: Invalid value for "CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color] + DEBUG Property: Found valid "CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color] + DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color] + + + and when setting an explicit default profile:: + + cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2 + s = parser.parseString('''body { + unknown-property: x; + color: 4; + color: rgba(1,2,3,4); + color: red + }''') + + # Log output: + + WARNING Property: Unknown Property name. [2:9: unknown-property] + ERROR Property: Invalid value for "CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color] + WARNING Property: Not valid for profile "CSS Level 2.1" but valid "CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color] + DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color] + """ + valid = False + + profiles = None + try: + # if @font-face use that profile + rule = self.parent.parentRule + except AttributeError: + pass + else: + if rule is not None: + if rule.type == rule.FONT_FACE_RULE: + profiles = [cssutils.profile.CSS3_FONT_FACE] + #TODO: same for @page + + if self.name and self.value: + + cv = self.propertyValue + # TODO +# if cv.cssValueType == cv.CSS_VARIABLE and not cv.value: +# # TODO: false alarms too! +# cssutils.log.warn(u'No value for variable "%s" found, keeping ' +# u'variable.' % cv.name, neverraise=True) + + if self.name in cssutils.profile.knownNames: + # add valid, matching, validprofiles... + valid, matching, validprofiles = \ + cssutils.profile.validateWithProfile(self.name, + self.value, + profiles) + + if not valid: + self._log.error(u'Property: Invalid value for ' + u'"%s" property: %s' + % (u'/'.join(validprofiles), self.value), + token=self.__nametoken, + neverraise=True) + + # TODO: remove logic to profiles! + elif valid and not matching:#(profiles and profiles not in validprofiles): + if not profiles: + notvalidprofiles = u'/'.join(cssutils.profile.defaultProfiles) + else: + notvalidprofiles = profiles + self._log.warn(u'Property: Not valid for profile "%s" ' + u'but valid "%s" value: %s ' + % (notvalidprofiles, u'/'.join(validprofiles), + self.value), + token = self.__nametoken, + neverraise=True) + valid = False + + elif valid: + self._log.debug(u'Property: Found valid "%s" value: %s' + % (u'/'.join(validprofiles), self.value), + token = self.__nametoken, + neverraise=True) + + if self._priority not in (u'', u'important'): + valid = False + + return valid + + valid = property(validate, doc=u"Check if value of this property is valid " + u"in the properties context.") + + + @Deprecated(u'Use ``property.propertyValue`` instead.') + def _getCSSValue(self): + return self.propertyValue + + @Deprecated(u'Use ``property.propertyValue`` instead.') + def _setCSSValue(self, cssText): + self._setPropertyValue(cssText) + + cssValue = property(_getCSSValue, _setCSSValue, + doc="(DEPRECATED) Use ``property.propertyValue`` instead.") diff --git a/libs/cssutils/css/selector.py b/libs/cssutils/css/selector.py new file mode 100755 index 00000000..87840a0d --- /dev/null +++ b/libs/cssutils/css/selector.py @@ -0,0 +1,813 @@ +"""Selector is a single Selector of a CSSStyleRule SelectorList. +Partly implements http://www.w3.org/TR/css3-selectors/. + +TODO + - .contains(selector) + - .isSubselector(selector) +""" +__all__ = ['Selector'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.helper import Deprecated +from cssutils.util import _SimpleNamespaces +import cssutils +import xml.dom + +class Selector(cssutils.util.Base2): + """ + (cssutils) a single selector in a :class:`~cssutils.css.SelectorList` + of a :class:`~cssutils.css.CSSStyleRule`. + + Format:: + + # implemented in SelectorList + selectors_group + : selector [ COMMA S* selector ]* + ; + + selector + : simple_selector_sequence [ combinator simple_selector_sequence ]* + ; + + combinator + /* combinators can be surrounded by white space */ + : PLUS S* | GREATER S* | TILDE S* | S+ + ; + + simple_selector_sequence + : [ type_selector | universal ] + [ HASH | class | attrib | pseudo | negation ]* + | [ HASH | class | attrib | pseudo | negation ]+ + ; + + type_selector + : [ namespace_prefix ]? element_name + ; + + namespace_prefix + : [ IDENT | '*' ]? '|' + ; + + element_name + : IDENT + ; + + universal + : [ namespace_prefix ]? '*' + ; + + class + : '.' IDENT + ; + + attrib + : '[' S* [ namespace_prefix ]? IDENT S* + [ [ PREFIXMATCH | + SUFFIXMATCH | + SUBSTRINGMATCH | + '=' | + INCLUDES | + DASHMATCH ] S* [ IDENT | STRING ] S* + ]? ']' + ; + + pseudo + /* '::' starts a pseudo-element, ':' a pseudo-class */ + /* Exceptions: :first-line, :first-letter, :before and :after. */ + /* Note that pseudo-elements are restricted to one per selector and */ + /* occur only in the last simple_selector_sequence. */ + : ':' ':'? [ IDENT | functional_pseudo ] + ; + + functional_pseudo + : FUNCTION S* expression ')' + ; + + expression + /* In CSS3, the expressions are identifiers, strings, */ + /* or of the form "an+b" */ + : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ + ; + + negation + : NOT S* negation_arg S* ')' + ; + + negation_arg + : type_selector | universal | HASH | class | attrib | pseudo + ; + + """ + def __init__(self, selectorText=None, parent=None, + readonly=False): + """ + :Parameters: + selectorText + initial value of this selector + parent + a SelectorList + readonly + default to False + """ + super(Selector, self).__init__() + + self.__namespaces = _SimpleNamespaces(log=self._log) + self._element = None + self._parent = parent + self._specificity = (0, 0, 0, 0) + + if selectorText: + self.selectorText = selectorText + + self._readonly = readonly + + def __repr__(self): + if self.__getNamespaces(): + st = (self.selectorText, self._getUsedNamespaces()) + else: + st = self.selectorText + return u"cssutils.css.%s(selectorText=%r)" % (self.__class__.__name__, + st) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.selectorText, + self.specificity, + self._getUsedNamespaces(), + id(self)) + + def _getUsedUris(self): + "Return list of actually used URIs in this Selector." + uris = set() + for item in self.seq: + type_, val = item.type, item.value + if type_.endswith(u'-selector') or type_ == u'universal' and \ + isinstance(val, tuple) and val[0] not in (None, u'*'): + uris.add(val[0]) + return uris + + def _getUsedNamespaces(self): + "Return actually used namespaces only." + useduris = self._getUsedUris() + namespaces = _SimpleNamespaces(log=self._log) + for p, uri in self._namespaces.items(): + if uri in useduris: + namespaces[p] = uri + return namespaces + + def __getNamespaces(self): + "Use own namespaces if not attached to a sheet, else the sheet's ones." + try: + return self._parent.parentRule.parentStyleSheet.namespaces + except AttributeError: + return self.__namespaces + + _namespaces = property(__getNamespaces, + doc=u"If this Selector is attached to a " + u"CSSStyleSheet the namespaces of that sheet " + u"are mirrored here. While the Selector (or " + u"parent SelectorList or parentRule(s) of that " + u"are not attached a own dict of {prefix: " + u"namespaceURI} is used.") + + + element = property(lambda self: self._element, + doc=u"Effective element target of this selector.") + + parent = property(lambda self: self._parent, + doc=u"(DOM) The SelectorList that contains this Selector " + u"or None if this Selector is not attached to a " + u"SelectorList.") + + def _getSelectorText(self): + """Return serialized format.""" + return cssutils.ser.do_css_Selector(self) + + def _setSelectorText(self, selectorText): + """ + :param selectorText: + parsable string or a tuple of (selectorText, dict-of-namespaces). + Given namespaces are ignored if this object is attached to a + CSSStyleSheet! + + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + # might be (selectorText, namespaces) + selectorText, namespaces = self._splitNamespacesOff(selectorText) + + try: + # uses parent stylesheets namespaces if available, + # otherwise given ones + namespaces = self.parent.parentRule.parentStyleSheet.namespaces + except AttributeError: + pass + tokenizer = self._tokenize2(selectorText) + if not tokenizer: + self._log.error(u'Selector: No selectorText given.') + else: + # prepare tokenlist: + # "*" -> type "universal" + # "*"|IDENT + "|" -> combined to "namespace_prefix" + # "|" -> type "namespace_prefix" + # "." + IDENT -> combined to "class" + # ":" + IDENT, ":" + FUNCTION -> pseudo-class + # FUNCTION "not(" -> negation + # "::" + IDENT, "::" + FUNCTION -> pseudo-element + tokens = [] + for t in tokenizer: + typ, val, lin, col = t + if val == u':' and tokens and\ + self._tokenvalue(tokens[-1]) == ':': + # combine ":" and ":" + tokens[-1] = (typ, u'::', lin, col) + + elif typ == 'IDENT' and tokens\ + and self._tokenvalue(tokens[-1]) == u'.': + # class: combine to .IDENT + tokens[-1] = ('class', u'.'+val, lin, col) + elif typ == 'IDENT' and tokens and \ + self._tokenvalue(tokens[-1]).startswith(u':') and\ + not self._tokenvalue(tokens[-1]).endswith(u'('): + # pseudo-X: combine to :IDENT or ::IDENT but not ":a(" + "b" + if self._tokenvalue(tokens[-1]).startswith(u'::'): + t = 'pseudo-element' + else: + t = 'pseudo-class' + tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col) + + elif typ == 'FUNCTION' and val == u'not(' and tokens and \ + u':' == self._tokenvalue(tokens[-1]): + tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3]) + elif typ == 'FUNCTION' and tokens\ + and self._tokenvalue(tokens[-1]).startswith(u':'): + # pseudo-X: combine to :FUNCTION( or ::FUNCTION( + if self._tokenvalue(tokens[-1]).startswith(u'::'): + t = 'pseudo-element' + else: + t = 'pseudo-class' + tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col) + + elif val == u'*' and tokens and\ + self._type(tokens[-1]) == 'namespace_prefix' and\ + self._tokenvalue(tokens[-1]).endswith(u'|'): + # combine prefix|* + tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val, + lin, col) + elif val == u'*': + # universal: "*" + tokens.append(('universal', val, lin, col)) + + elif val == u'|' and tokens and\ + self._type(tokens[-1]) in (self._prods.IDENT, 'universal')\ + and self._tokenvalue(tokens[-1]).find(u'|') == -1: + # namespace_prefix: "IDENT|" or "*|" + tokens[-1] = ('namespace_prefix', + self._tokenvalue(tokens[-1])+u'|', lin, col) + elif val == u'|': + # namespace_prefix: "|" + tokens.append(('namespace_prefix', val, lin, col)) + + else: + tokens.append(t) + + tokenizer = iter(tokens) + + # for closures: must be a mutable + new = {'context': [''], # stack of: 'attrib', 'negation', 'pseudo' + 'element': None, + '_PREFIX': None, + 'specificity': [0, 0, 0, 0], # mutable, finally a tuple! + 'wellformed': True + } + # used for equality checks and setting of a space combinator + S = u' ' + + def append(seq, val, typ=None, token=None): + """ + appends to seq + + namespace_prefix, IDENT will be combined to a tuple + (prefix, name) where prefix might be None, the empty string + or a prefix. + + Saved are also: + - specificity definition: style, id, class/att, type + - element: the element this Selector is for + """ + context = new['context'][-1] + if token: + line, col = token[2], token[3] + else: + line, col = None, None + + if typ == '_PREFIX': + # SPECIAL TYPE: save prefix for combination with next + new['_PREFIX'] = val[:-1] + # handle next time + return + + if new['_PREFIX'] is not None: + # as saved from before and reset to None + prefix, new['_PREFIX'] = new['_PREFIX'], None + elif typ == 'universal' and '|' in val: + # val == *|* or prefix|* + prefix, val = val.split('|') + else: + prefix = None + + # namespace + if (typ.endswith('-selector') or typ == 'universal') and not ( + 'attribute-selector' == typ and not prefix): + # att **IS NOT** in default ns + if prefix == u'*': + # *|name: in ANY_NS + namespaceURI = cssutils._ANYNS + elif prefix is None: + # e or *: default namespace with prefix u'' + # or local-name() + namespaceURI = namespaces.get(u'', None) + elif prefix == u'': + # |name or |*: in no (or the empty) namespace + namespaceURI = u'' + else: + # explicit namespace prefix + # does not raise KeyError, see _SimpleNamespaces + namespaceURI = namespaces[prefix] + + if namespaceURI is None: + new['wellformed'] = False + self._log.error(u'Selector: No namespaceURI found ' + u'for prefix %r' % prefix, + token=token, + error=xml.dom.NamespaceErr) + return + + # val is now (namespaceprefix, name) tuple + val = (namespaceURI, val) + + # specificity + if not context or context == 'negation': + if 'id' == typ: + new['specificity'][1] += 1 + elif 'class' == typ or '[' == val: + new['specificity'][2] += 1 + elif typ in ('type-selector', 'negation-type-selector', + 'pseudo-element'): + new['specificity'][3] += 1 + if not context and typ in ('type-selector', 'universal'): + # define element + new['element'] = val + + seq.append(val, typ, line=line, col=col) + + # expected constants + simple_selector_sequence = 'type_selector universal HASH class ' \ + 'attrib pseudo negation ' + simple_selector_sequence2 = 'HASH class attrib pseudo negation ' + + element_name = 'element_name' + + negation_arg = 'type_selector universal HASH class attrib pseudo' + negationend = ')' + + attname = 'prefix attribute' + attname2 = 'attribute' + attcombinator = 'combinator ]' # optional + attvalue = 'value' # optional + attend = ']' + + expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT' + expression = expressionstart + ' )' + + combinator = ' combinator' + + def _COMMENT(expected, seq, token, tokenizer=None): + "special implementation for comment token" + append(seq, cssutils.css.CSSComment([token]), 'COMMENT', + token=token) + return expected + + def _S(expected, seq, token, tokenizer=None): + # S + context = new['context'][-1] + if context.startswith('pseudo-'): + if seq and seq[-1].value not in u'+-': + # e.g. x:func(a + b) + append(seq, S, 'S', token=token) + return expected + + elif context != 'attrib' and 'combinator' in expected: + append(seq, S, 'descendant', token=token) + return simple_selector_sequence + combinator + + else: + return expected + + def _universal(expected, seq, token, tokenizer=None): + # *|* or prefix|* + context = new['context'][-1] + val = self._tokenvalue(token) + if 'universal' in expected: + append(seq, val, 'universal', token=token) + + if 'negation' == context: + return negationend + else: + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected universal.', token=token) + return expected + + def _namespace_prefix(expected, seq, token, tokenizer=None): + # prefix| => element_name + # or prefix| => attribute_name if attrib + context = new['context'][-1] + val = self._tokenvalue(token) + if 'attrib' == context and 'prefix' in expected: + # [PREFIX|att] + append(seq, val, '_PREFIX', token=token) + return attname2 + elif 'type_selector' in expected: + # PREFIX|* + append(seq, val, '_PREFIX', token=token) + return element_name + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected namespace prefix.', token=token) + return expected + + def _pseudo(expected, seq, token, tokenizer=None): + # pseudo-class or pseudo-element :a ::a :a( ::a( + """ + /* '::' starts a pseudo-element, ':' a pseudo-class */ + /* Exceptions: :first-line, :first-letter, :before and + :after. */ + /* Note that pseudo-elements are restricted to one per selector + and */ + /* occur only in the last simple_selector_sequence. */ + """ + context = new['context'][-1] + val, typ = self._tokenvalue(token, normalize=True),\ + self._type(token) + if 'pseudo' in expected: + if val in (':first-line', + ':first-letter', + ':before', + ':after'): + # always pseudo-element ??? + typ = 'pseudo-element' + append(seq, val, typ, token=token) + + if val.endswith(u'('): + # function + # "pseudo-" "class" or "element" + new['context'].append(typ) + return expressionstart + elif 'negation' == context: + return negationend + elif 'pseudo-element' == typ: + # only one per element, check at ) also! + return combinator + else: + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected start of pseudo.', token=token) + return expected + + def _expression(expected, seq, token, tokenizer=None): + # [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ + context = new['context'][-1] + val, typ = self._tokenvalue(token), self._type(token) + if context.startswith('pseudo-'): + append(seq, val, typ, token=token) + return expression + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected %s.' % typ, token=token) + return expected + + def _attcombinator(expected, seq, token, tokenizer=None): + # context: attrib + # PREFIXMATCH | SUFFIXMATCH | SUBSTRINGMATCH | INCLUDES | + # DASHMATCH + context = new['context'][-1] + val, typ = self._tokenvalue(token), self._type(token) + if 'attrib' == context and 'combinator' in expected: + # combinator in attrib + append(seq, val, typ.lower(), token=token) + return attvalue + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected %s.' % typ, token=token) + return expected + + def _string(expected, seq, token, tokenizer=None): + # identifier + context = new['context'][-1] + typ, val = self._type(token), self._stringtokenvalue(token) + + # context: attrib + if 'attrib' == context and 'value' in expected: + # attrib: [...=VALUE] + append(seq, val, typ, token=token) + return attend + + # context: pseudo + elif context.startswith('pseudo-'): + # :func(...) + append(seq, val, typ, token=token) + return expression + + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected STRING.', token=token) + return expected + + def _ident(expected, seq, token, tokenizer=None): + # identifier + context = new['context'][-1] + val, typ = self._tokenvalue(token), self._type(token) + + # context: attrib + if 'attrib' == context and 'attribute' in expected: + # attrib: [...|ATT...] + append(seq, val, 'attribute-selector', token=token) + return attcombinator + + elif 'attrib' == context and 'value' in expected: + # attrib: [...=VALUE] + append(seq, val, 'attribute-value', token=token) + return attend + + # context: negation + elif 'negation' == context: + # negation: (prefix|IDENT) + append(seq, val, 'negation-type-selector', token=token) + return negationend + + # context: pseudo + elif context.startswith('pseudo-'): + # :func(...) + append(seq, val, typ, token=token) + return expression + + elif 'type_selector' in expected or element_name == expected: + # element name after ns or complete type_selector + append(seq, val, 'type-selector', token=token) + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error(u'Selector: Unexpected IDENT.', token=token) + return expected + + def _class(expected, seq, token, tokenizer=None): + # .IDENT + context = new['context'][-1] + val = self._tokenvalue(token) + if 'class' in expected: + append(seq, val, 'class', token=token) + + if 'negation' == context: + return negationend + else: + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error(u'Selector: Unexpected class.', token=token) + return expected + + def _hash(expected, seq, token, tokenizer=None): + # #IDENT + context = new['context'][-1] + val = self._tokenvalue(token) + if 'HASH' in expected: + append(seq, val, 'id', token=token) + + if 'negation' == context: + return negationend + else: + return simple_selector_sequence2 + combinator + + else: + new['wellformed'] = False + self._log.error(u'Selector: Unexpected HASH.', token=token) + return expected + + def _char(expected, seq, token, tokenizer=None): + # + > ~ ) [ ] + - + context = new['context'][-1] + val = self._tokenvalue(token) + + # context: attrib + if u']' == val and 'attrib' == context and ']' in expected: + # end of attrib + append(seq, val, 'attribute-end', token=token) + context = new['context'].pop() # attrib is done + context = new['context'][-1] + if 'negation' == context: + return negationend + else: + return simple_selector_sequence2 + combinator + + elif u'=' == val and 'attrib' == context\ + and 'combinator' in expected: + # combinator in attrib + append(seq, val, 'equals', token=token) + return attvalue + + # context: negation + elif u')' == val and 'negation' == context and u')' in expected: + # not(negation_arg)" + append(seq, val, 'negation-end', token=token) + new['context'].pop() # negation is done + context = new['context'][-1] + return simple_selector_sequence + combinator + + # context: pseudo (at least one expression) + elif val in u'+-' and context.startswith('pseudo-'): + # :func(+ -)" + _names = {'+': 'plus', '-': 'minus'} + if val == u'+' and seq and seq[-1].value == S: + seq.replace(-1, val, _names[val]) + else: + append(seq, val, _names[val], + token=token) + return expression + + elif u')' == val and context.startswith('pseudo-') and\ + expression == expected: + # :func(expression)" + append(seq, val, 'function-end', token=token) + new['context'].pop() # pseudo is done + if 'pseudo-element' == context: + return combinator + else: + return simple_selector_sequence + combinator + + # context: ROOT + elif u'[' == val and 'attrib' in expected: + # start of [attrib] + append(seq, val, 'attribute-start', token=token) + new['context'].append('attrib') + return attname + + elif val in u'+>~' and 'combinator' in expected: + # no other combinator except S may be following + _names = { + '>': 'child', + '+': 'adjacent-sibling', + '~': 'following-sibling'} + if seq and seq[-1].value == S: + seq.replace(-1, val, _names[val]) + else: + append(seq, val, _names[val], token=token) + return simple_selector_sequence + + elif u',' == val: + # not a selectorlist + new['wellformed'] = False + self._log.error( + u'Selector: Single selector only.', + error=xml.dom.InvalidModificationErr, + token=token) + return expected + + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected CHAR.', token=token) + return expected + + def _negation(expected, seq, token, tokenizer=None): + # not( + context = new['context'][-1] + val = self._tokenvalue(token, normalize=True) + if 'negation' in expected: + new['context'].append('negation') + append(seq, val, 'negation-start', token=token) + return negation_arg + else: + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected negation.', token=token) + return expected + + def _atkeyword(expected, seq, token, tokenizer=None): + "invalidates selector" + new['wellformed'] = False + self._log.error( + u'Selector: Unexpected ATKEYWORD.', token=token) + return expected + + + # expected: only|not or mediatype, mediatype, feature, and + newseq = self._tempSeq() + + wellformed, expected = self._parse( + expected=simple_selector_sequence, + seq=newseq, tokenizer=tokenizer, + productions={'CHAR': _char, + 'class': _class, + 'HASH': _hash, + 'STRING': _string, + 'IDENT': _ident, + 'namespace_prefix': _namespace_prefix, + 'negation': _negation, + 'pseudo-class': _pseudo, + 'pseudo-element': _pseudo, + 'universal': _universal, + # pseudo + 'NUMBER': _expression, + 'DIMENSION': _expression, + # attribute + 'PREFIXMATCH': _attcombinator, + 'SUFFIXMATCH': _attcombinator, + 'SUBSTRINGMATCH': _attcombinator, + 'DASHMATCH': _attcombinator, + 'INCLUDES': _attcombinator, + + 'S': _S, + 'COMMENT': _COMMENT, + 'ATKEYWORD': _atkeyword}) + wellformed = wellformed and new['wellformed'] + + # post condition + if len(new['context']) > 1 or not newseq: + wellformed = False + self._log.error(u'Selector: Invalid or incomplete selector: %s' + % self._valuestr(selectorText)) + + if expected == 'element_name': + wellformed = False + self._log.error(u'Selector: No element name found: %s' + % self._valuestr(selectorText)) + + if expected == simple_selector_sequence and newseq: + wellformed = False + self._log.error(u'Selector: Cannot end with combinator: %s' + % self._valuestr(selectorText)) + + if newseq and hasattr(newseq[-1].value, 'strip') \ + and newseq[-1].value.strip() == u'': + del newseq[-1] + + # set + if wellformed: + self.__namespaces = namespaces + self._element = new['element'] + self._specificity = tuple(new['specificity']) + self._setSeq(newseq) + # filter that only used ones are kept + self.__namespaces = self._getUsedNamespaces() + + selectorText = property(_getSelectorText, _setSelectorText, + doc=u"(DOM) The parsable textual representation of " + u"the selector.") + + specificity = property(lambda self: self._specificity, + doc="""Specificity of this selector (READONLY). + Tuple of (a, b, c, d) where: + + a + presence of style in document, always 0 if not used on a + document + b + number of ID selectors + c + number of .class selectors + d + number of Element (type) selectors""") + + wellformed = property(lambda self: bool(len(self.seq))) + + + @Deprecated('Use property parent instead') + def _getParentList(self): + return self.parent + + parentList = property(_getParentList, + doc="DEPRECATED, see property parent instead") diff --git a/libs/cssutils/css/selectorlist.py b/libs/cssutils/css/selectorlist.py new file mode 100755 index 00000000..2072cd53 --- /dev/null +++ b/libs/cssutils/css/selectorlist.py @@ -0,0 +1,234 @@ +"""SelectorList is a list of CSS Selector objects. + +TODO + - remove duplicate Selectors. -> CSSOM canonicalize + + - ??? CSS2 gives a special meaning to the comma (,) in selectors. + However, since it is not known if the comma may acquire other + meanings in future versions of CSS, the whole statement should be + ignored if there is an error anywhere in the selector, even though + the rest of the selector may look reasonable in CSS2. + + Illegal example(s): + + For example, since the "&" is not a valid token in a CSS2 selector, + a CSS2 user agent must ignore the whole second line, and not set + the color of H3 to red: +""" +__all__ = ['SelectorList'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from selector import Selector +import cssutils +import xml.dom + +class SelectorList(cssutils.util.Base, cssutils.util.ListSeq): + """A list of :class:`~cssutils.css.Selector` objects + of a :class:`~cssutils.css.CSSStyleRule`.""" + def __init__(self, selectorText=None, parentRule=None, + readonly=False): + """ + :Parameters: + selectorText + parsable list of Selectors + parentRule + the parent CSSRule if available + """ + super(SelectorList, self).__init__() + + self._parentRule = parentRule + + if selectorText: + self.selectorText = selectorText + + self._readonly = readonly + + def __repr__(self): + if self._namespaces: + st = (self.selectorText, self._namespaces) + else: + st = self.selectorText + return u"cssutils.css.%s(selectorText=%r)" % (self.__class__.__name__, + st) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.selectorText, + self._namespaces, + id(self)) + + def __setitem__(self, index, newSelector): + """Overwrite ListSeq.__setitem__ + + Any duplicate Selectors are **not** removed. + """ + newSelector = self.__prepareset(newSelector) + if newSelector: + self.seq[index] = newSelector + + def __prepareset(self, newSelector, namespaces=None): + "Used by appendSelector and __setitem__" + if not namespaces: + namespaces = {} + self._checkReadonly() + if not isinstance(newSelector, Selector): + newSelector = Selector((newSelector, namespaces), + parent=self) + if newSelector.wellformed: + newSelector._parent = self # maybe set twice but must be! + return newSelector + + def __getNamespaces(self): + """Use children namespaces if not attached to a sheet, else the sheet's + ones. + """ + try: + return self.parentRule.parentStyleSheet.namespaces + except AttributeError: + namespaces = {} + for selector in self.seq: + namespaces.update(selector._namespaces) + return namespaces + + def _getUsedUris(self): + "Used by CSSStyleSheet to check if @namespace rules are needed" + uris = set() + for s in self: + uris.update(s._getUsedUris()) + return uris + + _namespaces = property(__getNamespaces, doc="""If this SelectorList is + attached to a CSSStyleSheet the namespaces of that sheet are mirrored + here. While the SelectorList (or parentRule(s) are + not attached the namespaces of all children Selectors are used.""") + + def append(self, newSelector): + "Same as :meth:`appendSelector`." + self.appendSelector(newSelector) + + def appendSelector(self, newSelector): + """ + Append `newSelector` to this list (a string will be converted to a + :class:`~cssutils.css.Selector`). + + :param newSelector: + comma-separated list of selectors (as a single string) or a tuple of + `(newSelector, dict-of-namespaces)` + :returns: New :class:`~cssutils.css.Selector` or ``None`` if + `newSelector` is not wellformed. + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + # might be (selectorText, namespaces) + newSelector, namespaces = self._splitNamespacesOff(newSelector) + try: + # use parent's only if available + namespaces = self.parentRule.parentStyleSheet.namespaces + except AttributeError: + # use already present namespaces plus new given ones + _namespaces = self._namespaces + _namespaces.update(namespaces) + namespaces = _namespaces + + newSelector = self.__prepareset(newSelector, namespaces) + if newSelector: + seq = self.seq[:] + del self.seq[:] + for s in seq: + if s.selectorText != newSelector.selectorText: + self.seq.append(s) + self.seq.append(newSelector) + return newSelector + + def _getSelectorText(self): + "Return serialized format." + return cssutils.ser.do_css_SelectorList(self) + + def _setSelectorText(self, selectorText): + """ + :param selectorText: + comma-separated list of selectors or a tuple of + (selectorText, dict-of-namespaces) + :exceptions: + - :exc:`~xml.dom.NamespaceErr`: + Raised if the specified selector uses an unknown namespace + prefix. + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + and is unparsable. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this rule is readonly. + """ + self._checkReadonly() + + # might be (selectorText, namespaces) + selectorText, namespaces = self._splitNamespacesOff(selectorText) + try: + # use parent's only if available + namespaces = self.parentRule.parentStyleSheet.namespaces + except AttributeError: + pass + + wellformed = True + tokenizer = self._tokenize2(selectorText) + newseq = [] + + expected = True + while True: + # find all upto and including next ",", EOF or nothing + selectortokens = self._tokensupto2(tokenizer, listseponly=True) + if selectortokens: + if self._tokenvalue(selectortokens[-1]) == ',': + expected = selectortokens.pop() + else: + expected = None + + selector = Selector((selectortokens, namespaces), + parent=self) + if selector.wellformed: + newseq.append(selector) + else: + wellformed = False + self._log.error(u'SelectorList: Invalid Selector: %s' % + self._valuestr(selectortokens)) + else: + break + + # post condition + if u',' == expected: + wellformed = False + self._log.error(u'SelectorList: Cannot end with ",": %r' % + self._valuestr(selectorText)) + elif expected: + wellformed = False + self._log.error(u'SelectorList: Unknown Syntax: %r' % + self._valuestr(selectorText)) + if wellformed: + self.seq = newseq + + selectorText = property(_getSelectorText, _setSelectorText, + doc=u"(cssutils) The textual representation of the " + u"selector for a rule set.") + + length = property(lambda self: len(self), + doc=u"The number of :class:`~cssutils.css.Selector` " + u"objects in the list.") + + parentRule = property(lambda self: self._parentRule, + doc=u"(DOM) The CSS rule that contains this " + u"SelectorList or ``None`` if this SelectorList " + u"is not attached to a CSSRule.") + + wellformed = property(lambda self: bool(len(self.seq))) + diff --git a/libs/cssutils/css/value.py b/libs/cssutils/css/value.py new file mode 100755 index 00000000..f9ab050a --- /dev/null +++ b/libs/cssutils/css/value.py @@ -0,0 +1,871 @@ +"""Value related classes. + +DOM Level 2 CSS CSSValue, CSSPrimitiveValue and CSSValueList are **no longer** +supported and are replaced by these new classes. +""" +__all__ = ['PropertyValue', + 'Value', + 'ColorValue', + 'DimensionValue', + 'URIValue', + 'CSSFunction', + 'CSSVariable', + 'MSValue' + ] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from cssutils.prodparser import * +import cssutils +from cssutils.helper import normalize, pushtoken +import colorsys +import math +import re +import xml.dom +import urlparse + +class PropertyValue(cssutils.util._NewBase): + """ + An unstructured list like holder for all values defined for a + :class:`~cssutils.css.Property`. Contains :class:`~cssutils.css.Value` + or subclass objects. Currently there is no access to the combinators of + the defined values which might simply be space or comma or slash. + + You may: + + - iterate over all contained Value objects (not the separators like ``,``, + ``/`` or `` `` though!) + - get a Value item by index or use ``PropertyValue[index]`` + - find out the number of values defined (unstructured) + """ + def __init__(self, cssText=None, parent=None, readonly=False): + """ + :param cssText: + the parsable cssText of the value + :param readonly: + defaults to False + """ + super(PropertyValue, self).__init__() + + self.parent = parent + self.wellformed = False + + if cssText is not None: # may be 0 + if isinstance(cssText, (int, float)): + cssText = unicode(cssText) # if it is a number + self.cssText = cssText + + self._readonly = readonly + + def __len__(self): + return len(list(self.__items())) + + def __getitem__(self, index): + try: + return list(self.__items())[index] + except IndexError: + return None + + def __iter__(self): + "Generator which iterates over values." + for item in self.__items(): + yield item + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u"" % (self.__class__.__name__, + self.length, self.cssText, id(self)) + + def __items(self, seq=None): + "a generator of Value obects only, no , / or ' '" + if seq is None: + seq = self.seq + return (x.value for x in seq if isinstance(x.value, Value)) + + def _setCssText(self, cssText): + if isinstance(cssText, (int, float)): + cssText = unicode(cssText) # if it is a number + """ + Format:: + + unary_operator + : '-' | '+' + ; + operator + : '/' S* | ',' S* | /* empty */ + ; + expr + : term [ operator term ]* + ; + term + : unary_operator? + [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | + ANGLE S* | TIME S* | FREQ S* ] + | STRING S* | IDENT S* | URI S* | hexcolor | function + | UNICODE-RANGE S* + ; + function + : FUNCTION S* expr ')' S* + ; + /* + * There is a constraint on the color that it must + * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) + * after the "#"; e.g., "#000" is OK, but "#abcd" is not. + */ + hexcolor + : HASH S* + ; + + :exceptions: + - :exc:`~xml.dom.SyntaxErr`: + Raised if the specified CSS string value has a syntax error + (according to the attached property) or is unparsable. + - :exc:`~xml.dom.InvalidModificationErr`: + TODO: Raised if the specified CSS string value represents a + different type of values than the values allowed by the CSS + property. + - :exc:`~xml.dom.NoModificationAllowedErr`: + Raised if this value is readonly. + """ + self._checkReadonly() + + # used as operator is , / or S + nextSor = u',/' + term = Choice(_ColorProd(self, nextSor), + _DimensionProd(self, nextSor), + _URIProd(self, nextSor), + _ValueProd(self, nextSor), +# _CalcValueProd(self, nextSor), +# _Rect(self, nextSor), + # all other functions + _CSSVariableProd(self, nextSor), + _MSValueProd(self, nextSor), + _CSSFunctionProd(self, nextSor) + ) + operator = Choice(PreDef.S(toSeq=False), + PreDef.char('comma', ',', + toSeq=lambda t, tokens: ('operator', t[1])), + PreDef.char('slash', '/', + toSeq=lambda t, tokens: ('operator', t[1])), + optional=True) + prods = Sequence(term, + Sequence(# mayEnd this Sequence if whitespace + operator, + # TODO: only when setting via other class + # used by variabledeclaration currently + PreDef.char('END', ';', + stopAndKeep=True, + optional=True), + # TODO: } and !important ends too! + term, + minmax=lambda: (0, None))) + # parse + ok, seq, store, unused = ProdParser().parse(cssText, + u'PropertyValue', + prods) + # must be at least one value! + ok = ok and len(list(self.__items(seq))) > 0 + if ok: + self._setSeq(seq) + self.wellformed = True + else: + self._log.error(u'PropertyValue: Unknown syntax or no value: %s' % + self._valuestr(cssText)) + + cssText = property(lambda self: cssutils.ser.do_css_PropertyValue(self), + _setCssText, + doc="A string representation of the current value.") + + def item(self, index): + """ + The value at position `index`. Alternatively simple use + ``PropertyValue[index]``. + + :param index: + the parsable cssText of the value + :exceptions: + - :exc:`~IndexError`: + Raised if index if out of bounds + """ + return self[index] + + length = property(lambda self: len(self), + doc=u"Number of values set.") + + value = property(lambda self: cssutils.ser.do_css_PropertyValue(self, + valuesOnly=True), + doc=u"A string representation of the current value " + u"without any comments used for validation.") + + +class Value(cssutils.util._NewBase): + """ + Represents a single CSS value. For now simple values of + IDENT, STRING, or UNICODE-RANGE values are represented directly + as Value objects. Other values like e.g. FUNCTIONs are represented by + subclasses with an extended API. + """ + IDENT = u'IDENT' + STRING = u'STRING' + UNICODE_RANGE = u'UNICODE-RANGE' + URI = u'URI' + + DIMENSION = u'DIMENSION' + NUMBER = u'NUMBER' + PERCENTAGE = u'PERCENTAGE' + + COLOR_VALUE = u'COLOR_VALUE' + HASH = u'HASH' + + FUNCTION = u'FUNCTION' + VARIABLE = u'VARIABLE' + + _type = None + _value = u'' + + def __init__(self, cssText=None, parent=None, readonly=False): + super(Value, self).__init__() + + self.parent = parent + + if cssText: + self.cssText = cssText + + def __repr__(self): + return u"cssutils.css.%s(%r)" % (self.__class__.__name__, + self.cssText) + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.type, self.value, self.cssText, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + prods = Choice(PreDef.hexcolor(stop=True), + PreDef.ident(stop=True), + PreDef.string(stop=True), + PreDef.unicode_range(stop=True), + ) + ok, seq, store, unused = ProdParser().parse(cssText, u'Value', prods) + if ok: + # only 1 value anyway! + self._type = seq[0].type + self._value = seq[0].value + + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_Value(self), + _setCssText, + doc=u'String value of this value.') + + type = property(lambda self: self._type, #_setType, + doc=u"Type of this value, for now the production type " + u"like e.g. `DIMENSION` or `STRING`. All types are " + u"defined as constants in :class:`~cssutils.css.Value`.") + + def _setValue(self, value): + # TODO: check! + self._value = value + + value = property(lambda self: self._value, _setValue, + doc=u"Actual value if possible: An int or float or else " + u" a string") + + +class ColorValue(Value): + """ + A color value like rgb(), rgba(), hsl(), hsla() or #rgb, #rrggbb + + TODO: Color Keywords + """ + from colors import COLORS + + type = Value.COLOR_VALUE + # hexcolor, FUNCTION? + _colorType = None + _red = 0 + _green = 0 + _blue = 0 + _alpha = 0 + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.type, self.value, + self.colorType, self.red, self.green, self.blue, self.alpha, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + types = self._prods # rename! + + component = Choice(PreDef.unary(toSeq=lambda t, tokens: (t[0], + DimensionValue(pushtoken(t, tokens), + parent=self) + )), + PreDef.number(toSeq=lambda t, tokens: (t[0], + DimensionValue(pushtoken(t, tokens), + parent=self) + )), + PreDef.percentage(toSeq=lambda t, tokens: (t[0], + DimensionValue(pushtoken(t, tokens), + parent=self) + )) + ) + noalp = Sequence(Prod(name='FUNCTION', + match=lambda t, v: t == types.FUNCTION and + v in (u'rgb(', u'hsl('), + toSeq=lambda t, tokens: (t[0], normalize(t[1]))), + component, + Sequence(PreDef.comma(), + component, + minmax=lambda: (2, 2) + ), + PreDef.funcEnd(stop=True) + ) + witha = Sequence(Prod(name='FUNCTION', + match=lambda t, v: t == types.FUNCTION and + v in (u'rgba(', u'hsla('), + toSeq=lambda t, tokens: (t[0], + normalize(t[1])) + ), + component, + Sequence(PreDef.comma(), + component, + minmax=lambda: (3, 3) + ), + PreDef.funcEnd(stop=True) + ) + namedcolor = Prod(name='Named Color', + match=lambda t, v: t == 'IDENT' and ( + normalize(v) in self.COLORS.keys() + ), + stop=True) + + prods = Choice(PreDef.hexcolor(stop=True), + namedcolor, + noalp, + witha) + + ok, seq, store, unused = ProdParser().parse(cssText, + self.type, + prods) + if ok: + t, v = seq[0].type, seq[0].value + if u'IDENT' == t: + rgba = self.COLORS[normalize(v)] + if u'HASH' == t: + if len(v) == 4: + # HASH #rgb + rgba = (int(2*v[1], 16), + int(2*v[2], 16), + int(2*v[3], 16), + 1.0) + else: + # HASH #rrggbb + rgba = (int(v[1:3], 16), + int(v[3:5], 16), + int(v[5:7], 16), + 1.0) + + elif u'FUNCTION' == t: + functiontype, raw, check = None, [], u'' + HSL = False + + for item in seq: + try: + type_ = item.value.type + except AttributeError, e: + # type of function, e.g. rgb( + if item.type == 'FUNCTION': + functiontype = item.value + HSL = functiontype in (u'hsl(', u'hsla(') + continue + + # save components + if type_ == Value.NUMBER: + raw.append(item.value.value) + check += u'N' + elif type_ == Value.PERCENTAGE: + if HSL: + # save as percentage fraction + raw.append(item.value.value / 100.0) + else: + # save as real value of percentage of 255 + raw.append(int(255 * item.value.value / 100)) + check += u'P' + + if HSL: + # convert to rgb + # h is 360 based (circle) + h, s, l = raw[0] / 360.0, raw[1], raw[2] + # ORDER h l s !!! + r, g, b = colorsys.hls_to_rgb(h, l, s) + # back to 255 based + rgba = [int(round(r*255)), + int(round(g*255)), + int(round(b*255))] + + if len(raw) > 3: + rgba.append(raw[3]) + + else: + # rgb, rgba + rgba = raw + + if len(rgba) < 4: + rgba.append(1.0) + + # validate + checks = {u'rgb(': ('NNN', 'PPP'), + u'rgba(': ('NNNN', 'PPPN'), + u'hsl(': ('NPP',), + u'hsla(': ('NPPN',) + } + if check not in checks[functiontype]: + self._log.error(u'ColorValue has invalid %s) parameters: ' + u'%s (N=Number, P=Percentage)' % + (functiontype, check)) + + self._colorType = t + self._red, self._green, self._blue, self._alpha = tuple(rgba) + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_ColorValue(self), + _setCssText, + doc=u"String value of this value.") + + value = property(lambda self: cssutils.ser.do_css_CSSFunction(self, True), + doc=u'Same as cssText but without comments.') + + type = property(lambda self: Value.COLOR_VALUE, + doc=u"Type is fixed to Value.COLOR_VALUE.") + + def _getName(self): + for n, v in self.COLORS.items(): + if v == (self.red, self.green, self.blue, self.alpha): + return n + + colorType = property(lambda self: self._colorType, + doc=u"IDENT (red), HASH (#f00) or FUNCTION (rgb(255, 0, 0).") + + name = property(_getName, + doc=u'Name of the color if known (in ColorValue.COLORS) ' + u'else None') + + red = property(lambda self: self._red, + doc=u'red part as integer between 0 and 255') + green = property(lambda self: self._green, + doc=u'green part as integer between 0 and 255') + blue = property(lambda self: self._blue, + doc=u'blue part as integer between 0 and 255') + alpha = property(lambda self: self._alpha, + doc=u'alpha part as float between 0.0 and 1.0') + +class DimensionValue(Value): + """ + A numerical value with an optional dimenstion like e.g. "px" or "%". + + Covers DIMENSION, PERCENTAGE or NUMBER values. + """ + __reNumDim = re.compile(ur'^(\d*\.\d+|\d+)(.*)$', re.I | re.U | re.X) + _dimension = None + _sign = None + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.type, self.value, self.dimension, self.cssText, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + prods = Sequence(PreDef.unary(), + Choice(PreDef.dimension(stop=True), + PreDef.number(stop=True), + PreDef.percentage(stop=True) + ) + ) + ok, seq, store, unused = ProdParser().parse(cssText, + u'DimensionValue', + prods) + if ok: + sign = val = u'' + dim = type_ = None + + # find + for item in seq: + if item.value in u'+-': + sign = item.value + else: + type_ = item.type + + # number + optional dim + v, d = self.__reNumDim.findall( + normalize(item.value))[0] + if u'.' in v: + val = float(sign + v) + else: + val = int(sign + v) + if d: + dim = d + + self._sign = sign + self._value = val + self._dimension = dim + self._type = type_ + + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_Value(self), + _setCssText, + doc=u"String value of this value including dimension.") + + dimension = property(lambda self: self._dimension, #_setValue, + doc=u"Dimension if a DIMENSION or PERCENTAGE value, " + u"else None") +class URIValue(Value): + """ + An URI value like ``url(example.png)``. + """ + _type = Value.URI + _uri = Value._value + + def __str__(self): + return u""\ + % (self.__class__.__name__, + self.type, self.value, self.uri, self.cssText, + id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + prods = Sequence(PreDef.uri(stop=True)) + + ok, seq, store, unused = ProdParser().parse(cssText, u'URIValue', prods) + if ok: + # only 1 value only anyway + self._type = seq[0].type + self._value = seq[0].value + + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_Value(self), + _setCssText, + doc=u'String value of this value.') + + def _setUri(self, uri): + # TODO: check? + self._value = uri + + uri = property(lambda self: self._value, _setUri, + doc=u"Actual URL without delimiters or the empty string") + + def absoluteUri(self): + """Actual URL, made absolute if possible, else same as `uri`.""" + # Ancestry: PropertyValue, Property, CSSStyleDeclaration, CSSStyleRule, + # CSSStyleSheet + try: + # TODO: better way? + styleSheet = self.parent.parent.parent.parentRule.parentStyleSheet + except AttributeError, e: + return self.uri + else: + return urlparse.urljoin(styleSheet.href, self.uri) + + absoluteUri = property(absoluteUri, doc=absoluteUri.__doc__) + + +class CSSFunction(Value): + """ + A function value. + """ + _functionName = 'Function' + + def _productions(self): + """Return definition used for parsing.""" + types = self._prods # rename! + + itemProd = Choice(_ColorProd(self), + _DimensionProd(self), + _URIProd(self), + _ValueProd(self), + #_CalcValueProd(self), + _CSSVariableProd(self), + _CSSFunctionProd(self) + ) + funcProds = Sequence(Prod(name='FUNCTION', + match=lambda t, v: t == types.FUNCTION, + toSeq=lambda t, tokens: (t[0], + normalize(t[1]))), + Choice(Sequence(itemProd, + Sequence(PreDef.comma(), + itemProd, + minmax=lambda: (0, None)), + PreDef.funcEnd(stop=True)), + PreDef.funcEnd(stop=True)) + ) + return funcProds + + def _setCssText(self, cssText): + self._checkReadonly() + ok, seq, store, unused = ProdParser().parse(cssText, + self.type, + self._productions()) + if ok: + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_CSSFunction(self), + _setCssText, + doc=u"String value of this value.") + + value = property(lambda self: cssutils.ser.do_css_CSSFunction(self, True), + doc=u'Same as cssText but without comments.') + + type = property(lambda self: Value.FUNCTION, + doc=u"Type is fixed to Value.FUNCTION.") + +class MSValue(CSSFunction): + """An IE specific Microsoft only function value which is much looser + in what is syntactically allowed.""" + _functionName = 'MSValue' + + def _productions(self): + """Return definition used for parsing.""" + types = self._prods # rename! + + func = Prod(name='MSValue-Sub', + match=lambda t, v: t == self._prods.FUNCTION, + toSeq=lambda t, tokens: (MSValue._functionName, + MSValue(pushtoken(t, + tokens + ), + parent=self + ) + ) + ) + + + funcProds = Sequence(Prod(name='FUNCTION', + match=lambda t, v: t == types.FUNCTION, + toSeq=lambda t, tokens: (t[0], t[1]) + ), + Sequence(Choice(_ColorProd(self), + _DimensionProd(self), + _URIProd(self), + _ValueProd(self), + _MSValueProd(self), + #_CalcValueProd(self), + _CSSVariableProd(self), + func, + #_CSSFunctionProd(self), + Prod(name='MSValuePart', + match=lambda t, v: v != u')', + toSeq=lambda t, tokens: (t[0], t[1]) + ) + ), + minmax=lambda: (0, None) + ), + PreDef.funcEnd(stop=True) + ) + return funcProds + + def _setCssText(self, cssText): + super(MSValue, self)._setCssText(cssText) + + cssText = property(lambda self: cssutils.ser.do_css_MSValue(self), + _setCssText, + doc=u"String value of this value.") + + +class CSSVariable(CSSFunction): + """The CSSVariable represents a CSS variables like ``var(varname)``. + + A variable has a (nonnormalized!) `name` and a `value` which is + tried to be resolved from any available CSSVariablesRule definition. + """ + _functionName = 'CSSVariable' + _name = None + + def __str__(self): + return u"" % ( + self.__class__.__name__, self.name, self.value, id(self)) + + def _setCssText(self, cssText): + self._checkReadonly() + + types = self._prods # rename! + prods = Sequence(Prod(name='var', + match=lambda t, v: t == types.FUNCTION and + normalize(v) == u'var(' + ), + PreDef.ident(toStore='ident'), + PreDef.funcEnd(stop=True)) + + # store: name of variable + store = {'ident': None} + ok, seq, store, unused = ProdParser().parse(cssText, + u'CSSVariable', + prods) + if ok: + self._name = store['ident'].value + self._setSeq(seq) + self.wellformed = ok + + cssText = property(lambda self: cssutils.ser.do_css_CSSVariable(self), + _setCssText, doc=u"String representation of variable.") + + # TODO: writable? check if var (value) available? + name = property(lambda self: self._name, + doc=u"The name identifier of this variable referring to " + u"a value in a " + u":class:`cssutils.css.CSSVariablesDeclaration`.") + + type = property(lambda self: Value.VARIABLE, + doc=u"Type is fixed to Value.VARIABLE.") + + def _getValue(self): + "Find contained sheet and @variables there" + rel = self + while True: + # find node which has parentRule to get to StyleSheet + if hasattr(rel, 'parent'): + rel = rel.parent + else: + break + try: + variables = rel.parentRule.parentStyleSheet.variables + except AttributeError: + return None + else: + try: + return variables[self.name] + except KeyError: + return None + + value = property(_getValue, + doc=u'The resolved actual value or None.') + + +# helper for productions +def _ValueProd(parent, nextSor=False): + return Prod(name='Value', + match=lambda t, v: t in ('IDENT', 'STRING', 'UNICODE-RANGE'), + nextSor = nextSor, + toSeq=lambda t, tokens: ('Value', Value( + pushtoken(t, + tokens), + parent=parent) + ) + ) + + +def _DimensionProd(parent, nextSor=False): + return Prod(name='Dimension', + match=lambda t, v: t in (u'DIMENSION', + u'NUMBER', + u'PERCENTAGE') or v in u'+-', + nextSor = nextSor, + toSeq=lambda t, tokens: (t[0], DimensionValue( + pushtoken(t, + tokens), + parent=parent) + ) + ) + +def _URIProd(parent, nextSor=False): + return Prod(name='URIValue', + match=lambda t, v: t == 'URI', + nextSor = nextSor, + toSeq=lambda t, tokens: ('URIValue', URIValue( + pushtoken(t, + tokens), + parent=parent) + ) + ) + +reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$') + +def _ColorProd(parent, nextSor=False): + return Prod(name='ColorValue', + match=lambda t, v: + (t == 'HASH' and + reHexcolor.match(v) + ) or + (t == 'FUNCTION' and + normalize(v) in (u'rgb(', + u'rgba(', + u'hsl(', + u'hsla(') + ) or + (t == 'IDENT' and + normalize(v) in ColorValue.COLORS.keys() + ), + nextSor = nextSor, + toSeq=lambda t, tokens: ('ColorValue', ColorValue( + pushtoken(t, + tokens), + parent=parent) + ) + ) + +def _CSSFunctionProd(parent, nextSor=False): + return PreDef.function(nextSor=nextSor, + toSeq=lambda t, tokens: (CSSFunction._functionName, + CSSFunction( + pushtoken(t, tokens), + parent=parent) + ) + ) + +def _CSSVariableProd(parent, nextSor=False): + return PreDef.variable(nextSor=nextSor, + toSeq=lambda t, tokens: (CSSVariable._functionName, + CSSVariable( + pushtoken(t, tokens), + parent=parent) + ) + ) + +def _MSValueProd(parent, nextSor=False): + return Prod(name=MSValue._functionName, + match=lambda t, v: (#t == self._prods.FUNCTION and ( + normalize(v) in (u'expression(', + u'alpha(', + u'blur(', + u'chroma(', + u'dropshadow(', + u'fliph(', + u'flipv(', + u'glow(', + u'gray(', + u'invert(', + u'mask(', + u'shadow(', + u'wave(', + u'xray(') or + v.startswith(u'progid:DXImageTransform.Microsoft.') + ), + nextSor=nextSor, + toSeq=lambda t, tokens: (MSValue._functionName, + MSValue(pushtoken(t, + tokens + ), + parent=parent + ) + ) + ) diff --git a/libs/cssutils/css2productions.py b/libs/cssutils/css2productions.py new file mode 100755 index 00000000..a435e71e --- /dev/null +++ b/libs/cssutils/css2productions.py @@ -0,0 +1,131 @@ +"""productions for CSS 2.1 + +CSS2_1_MACROS and CSS2_1_PRODUCTIONS are from both +http://www.w3.org/TR/CSS21/grammar.html and +http://www.w3.org/TR/css3-syntax/#grammar0 + + +""" +__all__ = ['CSSProductions', 'MACROS', 'PRODUCTIONS'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +# option case-insensitive +MACROS = { + 'h': r'[0-9a-f]', + #'nonascii': r'[\200-\377]', + 'nonascii': r'[^\0-\177]', # CSS3 + 'unicode': r'\\{h}{1,6}(\r\n|[ \t\r\n\f])?', + + 'escape': r'{unicode}|\\[^\r\n\f0-9a-f]', + 'nmstart': r'[_a-zA-Z]|{nonascii}|{escape}', + 'nmchar': r'[_a-zA-Z0-9-]|{nonascii}|{escape}', + 'string1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*\"', + 'string2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*\'", + 'invalid1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*', + 'invalid2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*", + 'comment': r'\/\*[^*]*\*+([^/*][^*]*\*+)*\/', + # CSS list 080725 19:43 + # \/\*([^*\\]|{escape})*\*+(([^/*\\]|{escape})[^*]*\*+)*\/ + + 'ident': r'[-]?{nmstart}{nmchar}*', + 'name': r'{nmchar}+', + # CHANGED TO SPEC: added "-?" + 'num': r'-?[0-9]*\.[0-9]+|[0-9]+', + 'string': r'{string1}|{string2}', + 'invalid': r'{invalid1}|{invalid2}', + 'url': r'([!#$%&*-~]|{nonascii}|{escape})*', + 's': r'[ \t\r\n\f]+', + 'w': r'{s}?', + 'nl': r'\n|\r\n|\r|\f', + 'range': r'\?{1,6}|{h}(\?{0,5}|{h}(\?{0,4}|{h}(\?{0,3}|{h}(\?{0,2}|{h}(\??|{h})))))', + + 'A': r'a|\\0{0,4}(41|61)(\r\n|[ \t\r\n\f])?', + 'C': r'c|\\0{0,4}(43|63)(\r\n|[ \t\r\n\f])?', + 'D': r'd|\\0{0,4}(44|64)(\r\n|[ \t\r\n\f])?', + 'E': r'e|\\0{0,4}(45|65)(\r\n|[ \t\r\n\f])?', + 'F': r'f|\\0{0,4}(46|66)(\r\n|[ \t\r\n\f])?', + 'G': r'g|\\0{0,4}(47|67)(\r\n|[ \t\r\n\f])?|\\g', + 'H': r'h|\\0{0,4}(48|68)(\r\n|[ \t\r\n\f])?|\\h', + 'I': r'i|\\0{0,4}(49|69)(\r\n|[ \t\r\n\f])?|\\i', + 'K': r'k|\\0{0,4}(4b|6b)(\r\n|[ \t\r\n\f])?|\\k', + 'M': r'm|\\0{0,4}(4d|6d)(\r\n|[ \t\r\n\f])?|\\m', + 'N': r'n|\\0{0,4}(4e|6e)(\r\n|[ \t\r\n\f])?|\\n', + 'O': r'o|\\0{0,4}(51|71)(\r\n|[ \t\r\n\f])?|\\o', + 'P': r'p|\\0{0,4}(50|70)(\r\n|[ \t\r\n\f])?|\\p', + 'R': r'r|\\0{0,4}(52|72)(\r\n|[ \t\r\n\f])?|\\r', + 'S': r's|\\0{0,4}(53|73)(\r\n|[ \t\r\n\f])?|\\s', + 'T': r't|\\0{0,4}(54|74)(\r\n|[ \t\r\n\f])?|\\t', + 'X': r'x|\\0{0,4}(58|78)(\r\n|[ \t\r\n\f])?|\\x', + 'Z': r'z|\\0{0,4}(5a|7a)(\r\n|[ \t\r\n\f])?|\\z', + } + +PRODUCTIONS = [ + ('URI', r'url\({w}{string}{w}\)'), #"url("{w}{string}{w}")" {return URI;} + ('URI', r'url\({w}{url}{w}\)'), #"url("{w}{url}{w}")" {return URI;} + ('FUNCTION', r'{ident}\('), #{ident}"(" {return FUNCTION;} + + ('IMPORT_SYM', r'@{I}{M}{P}{O}{R}{T}'), #"@import" {return IMPORT_SYM;} + ('PAGE_SYM', r'@{P}{A}{G}{E}'), #"@page" {return PAGE_SYM;} + ('MEDIA_SYM', r'@{M}{E}{D}{I}{A}'), #"@media" {return MEDIA_SYM;} + ('FONT_FACE_SYM', r'@{F}{O}{N}{T}\-{F}{A}{C}{E}'), #"@font-face" {return FONT_FACE_SYM;} + + # CHANGED TO SPEC: only @charset + ('CHARSET_SYM', r'@charset '), #"@charset " {return CHARSET_SYM;} + + ('NAMESPACE_SYM', r'@{N}{A}{M}{E}{S}{P}{A}{C}{E}'), #"@namespace" {return NAMESPACE_SYM;} + + # CHANGED TO SPEC: ATKEYWORD + ('ATKEYWORD', r'\@{ident}'), + + ('IDENT', r'{ident}'), #{ident} {return IDENT;} + ('STRING', r'{string}'), #{string} {return STRING;} + ('INVALID', r'{invalid}'), # {return INVALID; /* unclosed string */} + ('HASH', r'\#{name}'), #"#"{name} {return HASH;} + ('PERCENTAGE', r'{num}%'), #{num}% {return PERCENTAGE;} + ('LENGTH', r'{num}{E}{M}'), #{num}em {return EMS;} + ('LENGTH', r'{num}{E}{X}'), #{num}ex {return EXS;} + ('LENGTH', r'{num}{P}{X}'), #{num}px {return LENGTH;} + ('LENGTH', r'{num}{C}{M}'), #{num}cm {return LENGTH;} + ('LENGTH', r'{num}{M}{M}'), #{num}mm {return LENGTH;} + ('LENGTH', r'{num}{I}{N}'), #{num}in {return LENGTH;} + ('LENGTH', r'{num}{P}{T}'), #{num}pt {return LENGTH;} + ('LENGTH', r'{num}{P}{C}'), #{num}pc {return LENGTH;} + ('ANGLE', r'{num}{D}{E}{G}'), #{num}deg {return ANGLE;} + ('ANGLE', r'{num}{R}{A}{D}'), #{num}rad {return ANGLE;} + ('ANGLE', r'{num}{G}{R}{A}{D}'), #{num}grad {return ANGLE;} + ('TIME', r'{num}{M}{S}'), #{num}ms {return TIME;} + ('TIME', r'{num}{S}'), #{num}s {return TIME;} + ('FREQ', r'{num}{H}{Z}'), #{num}Hz {return FREQ;} + ('FREQ', r'{num}{K}{H}{Z}'), #{num}kHz {return FREQ;} + ('DIMEN', r'{num}{ident}'), #{num}{ident} {return DIMEN;} + ('NUMBER', r'{num}'), #{num} {return NUMBER;} + #('UNICODERANGE', r'U\+{range}'), #U\+{range} {return UNICODERANGE;} + #('UNICODERANGE', r'U\+{h}{1,6}-{h}{1,6}'), #U\+{h}{1,6}-{h}{1,6} {return UNICODERANGE;} + # --- CSS3 --- + ('UNICODE-RANGE', r'[0-9A-F?]{1,6}(\-[0-9A-F]{1,6})?'), + ('CDO', r'\<\!\-\-'), #"" {return CDC;} + ('S', r'{s}'),# {return S;} + + # \/\*[^*]*\*+([^/*][^*]*\*+)*\/ /* ignore comments */ + # {s}+\/\*[^*]*\*+([^/*][^*]*\*+)*\/ {unput(' '); /*replace by space*/} + + ('INCLUDES', r'\~\='), #"~=" {return INCLUDES;} + ('DASHMATCH', r'\|\='), #"|=" {return DASHMATCH;} + ('LBRACE', r'\{'), #{w}"{" {return LBRACE;} + ('PLUS', r'\+'), #{w}"+" {return PLUS;} + ('GREATER', r'\>'), #{w}">" {return GREATER;} + ('COMMA', r'\,'), #{w}"," {return COMMA;} + ('IMPORTANT_SYM', r'\!({w}|{comment})*{I}{M}{P}{O}{R}{T}{A}{N}{T}'), #"!{w}important" {return IMPORTANT_SYM;} + ('COMMENT', '\/\*[^*]*\*+([^/][^*]*\*+)*\/'), # /* ignore comments */ + ('CLASS', r'\.'), #. {return *yytext;} + + # --- CSS3! --- + ('CHAR', r'[^"\']'), + ] + +class CSSProductions(object): + pass +for i, t in enumerate(PRODUCTIONS): + setattr(CSSProductions, t[0].replace('-', '_'), t[0]) \ No newline at end of file diff --git a/libs/cssutils/cssproductions.py b/libs/cssutils/cssproductions.py new file mode 100755 index 00000000..9f7b3d80 --- /dev/null +++ b/libs/cssutils/cssproductions.py @@ -0,0 +1,124 @@ +"""productions for cssutils based on a mix of CSS 2.1 and CSS 3 Syntax +productions + +- http://www.w3.org/TR/css3-syntax +- http://www.w3.org/TR/css3-syntax/#grammar0 + +open issues + - numbers contain "-" if present + - HASH: #aaa is, #000 is not anymore, + CSS2.1: 'nmchar': r'[_a-z0-9-]|{nonascii}|{escape}', + CSS3: 'nmchar': r'[_a-z-]|{nonascii}|{escape}', +""" +__all__ = ['CSSProductions', 'MACROS', 'PRODUCTIONS'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +# a complete list of css3 macros +MACROS = { + 'nonascii': r'[^\0-\177]', + 'unicode': r'\\[0-9A-Fa-f]{1,6}(?:{nl}|{s})?', + #'escape': r'{unicode}|\\[ -~\200-\777]', + 'escape': r'{unicode}|\\[^\n\r\f0-9a-f]', + 'nmstart': r'[_a-zA-Z]|{nonascii}|{escape}', + 'nmchar': r'[-_a-zA-Z0-9]|{nonascii}|{escape}', + 'string1': r'"([^\n\r\f\\"]|\\{nl}|{escape})*"', + 'string2': r"'([^\n\r\f\\']|\\{nl}|{escape})*'", + 'invalid1': r'\"([^\n\r\f\\"]|\\{nl}|{escape})*', + 'invalid2': r"\'([^\n\r\f\\']|\\{nl}|{escape})*", + + 'comment': r'\/\*[^*]*\*+([^/][^*]*\*+)*\/', + 'ident': r'[-]?{nmstart}{nmchar}*', + 'name': r'{nmchar}+', + 'num': r'[0-9]*\.[0-9]+|[0-9]+', #r'[-]?\d+|[-]?\d*\.\d+', + 'string': r'{string1}|{string2}', + # from CSS2.1 + 'invalid': r'{invalid1}|{invalid2}', + 'url': r'[\x09\x21\x23-\x26\x28\x2a-\x7E]|{nonascii}|{escape}', + + 's': r'\t|\r|\n|\f|\x20', + 'w': r'{s}*', + 'nl': r'\n|\r\n|\r|\f', + + 'A': r'A|a|\\0{0,4}(?:41|61)(?:\r\n|[ \t\r\n\f])?', + 'B': r'B|b|\\0{0,4}(?:42|62)(?:\r\n|[ \t\r\n\f])?', + 'C': r'C|c|\\0{0,4}(?:43|63)(?:\r\n|[ \t\r\n\f])?', + 'D': r'D|d|\\0{0,4}(?:44|64)(?:\r\n|[ \t\r\n\f])?', + 'E': r'E|e|\\0{0,4}(?:45|65)(?:\r\n|[ \t\r\n\f])?', + 'F': r'F|f|\\0{0,4}(?:46|66)(?:\r\n|[ \t\r\n\f])?', + 'G': r'G|g|\\0{0,4}(?:47|67)(?:\r\n|[ \t\r\n\f])?|\\G|\\g', + 'H': r'H|h|\\0{0,4}(?:48|68)(?:\r\n|[ \t\r\n\f])?|\\H|\\h', + 'I': r'I|i|\\0{0,4}(?:49|69)(?:\r\n|[ \t\r\n\f])?|\\I|\\i', + 'K': r'K|k|\\0{0,4}(?:4b|6b)(?:\r\n|[ \t\r\n\f])?|\\K|\\k', + 'L': r'L|l|\\0{0,4}(?:4c|6c)(?:\r\n|[ \t\r\n\f])?|\\L|\\l', + 'M': r'M|m|\\0{0,4}(?:4d|6d)(?:\r\n|[ \t\r\n\f])?|\\M|\\m', + 'N': r'N|n|\\0{0,4}(?:4e|6e)(?:\r\n|[ \t\r\n\f])?|\\N|\\n', + 'O': r'O|o|\\0{0,4}(?:4f|6f)(?:\r\n|[ \t\r\n\f])?|\\O|\\o', + 'P': r'P|p|\\0{0,4}(?:50|70)(?:\r\n|[ \t\r\n\f])?|\\P|\\p', + 'R': r'R|r|\\0{0,4}(?:52|72)(?:\r\n|[ \t\r\n\f])?|\\R|\\r', + 'S': r'S|s|\\0{0,4}(?:53|73)(?:\r\n|[ \t\r\n\f])?|\\S|\\s', + 'T': r'T|t|\\0{0,4}(?:54|74)(?:\r\n|[ \t\r\n\f])?|\\T|\\t', + 'U': r'U|u|\\0{0,4}(?:55|75)(?:\r\n|[ \t\r\n\f])?|\\U|\\u', + 'V': r'V|v|\\0{0,4}(?:56|76)(?:\r\n|[ \t\r\n\f])?|\\V|\\v', + 'X': r'X|x|\\0{0,4}(?:58|78)(?:\r\n|[ \t\r\n\f])?|\\X|\\x', + 'Z': r'Z|z|\\0{0,4}(?:5a|7a)(?:\r\n|[ \t\r\n\f])?|\\Z|\\z', + } + +# The following productions are the complete list of tokens +# used by cssutils, a mix of CSS3 and some CSS2.1 productions. +# The productions are **ordered**: +PRODUCTIONS = [ + # UTF8_BOM or UTF8_BOM_SIG will only be checked at beginning of CSS + ('BOM', '\xfe\xff|\xef\xbb\xbf'), + + ('S', r'{s}+'), # 1st in list of general productions + ('URI', r'{U}{R}{L}\({w}({string}|{url}*){w}\)'), + ('FUNCTION', r'{ident}\('), + ('UNICODE-RANGE', r'{U}\+[0-9A-Fa-f?]{1,6}(\-[0-9A-Fa-f]{1,6})?'), + ('IDENT', r'{ident}'), + ('DIMENSION', r'{num}{ident}'), + ('PERCENTAGE', r'{num}\%'), + ('NUMBER', r'{num}'), + ('HASH', r'\#{name}'), + ('COMMENT', r'{comment}'), #r'\/\*[^*]*\*+([^/][^*]*\*+)*\/'), + ('STRING', r'{string}'), + ('INVALID', r'{invalid}'), # from CSS2.1 + ('ATKEYWORD', r'@{ident}'), # other keywords are done in the tokenizer + ('INCLUDES', '\~\='), + ('DASHMATCH', r'\|\='), + ('PREFIXMATCH', r'\^\='), + ('SUFFIXMATCH', r'\$\='), + ('SUBSTRINGMATCH', r'\*\='), + ('CDO', r'\<\!\-\-'), + ('CDC', r'\-\-\>'), + ('CHAR', r'[^"\']') # MUST always be last + # valid ony at start so not checked everytime + #('CHARSET_SYM', r'@charset '), # from Errata includes ending space! + # checked specially if fullsheet is parsed + ] + + + +class CSSProductions(object): + """ + most attributes are set later + """ + EOF = True + # removed from productions as they simply are ATKEYWORD until + # tokenizing + CHARSET_SYM = u'CHARSET_SYM' + FONT_FACE_SYM = u'FONT_FACE_SYM' + MEDIA_SYM = u'MEDIA_SYM' + IMPORT_SYM = u'IMPORT_SYM' + NAMESPACE_SYM = u'NAMESPACE_SYM' + PAGE_SYM = u'PAGE_SYM' + VARIABLES_SYM = u'VARIABLES_SYM' + +for i, t in enumerate(PRODUCTIONS): + setattr(CSSProductions, t[0].replace('-', '_'), t[0]) + + +# may be enabled by settings.set +_DXImageTransform = (u'FUNCTION', + ur'progid\:DXImageTransform\.Microsoft\..+\(' + ) diff --git a/libs/cssutils/errorhandler.py b/libs/cssutils/errorhandler.py new file mode 100755 index 00000000..916adff5 --- /dev/null +++ b/libs/cssutils/errorhandler.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +"""cssutils ErrorHandler + +ErrorHandler + used as log with usual levels (debug, info, warn, error) + + if instanciated with ``raiseExceptions=True`` raises exeptions instead + of logging + +log + defaults to instance of ErrorHandler for any kind of log message from + lexerm, parser etc. + + - raiseExceptions = [False, True] + - setloglevel(loglevel) +""" +__all__ = ['ErrorHandler'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +import logging +import urllib2 +import xml.dom + +class _ErrorHandler(object): + """ + handles all errors and log messages + """ + def __init__(self, log, defaultloglevel=logging.INFO, + raiseExceptions=True): + """ + inits log if none given + + log + for parse messages, default logs to sys.stderr + defaultloglevel + if none give this is logging.DEBUG + raiseExceptions + - True: Errors will be raised e.g. during building + - False: Errors will be written to the log, this is the + default behaviour when parsing + """ + # may be disabled during setting of known valid items + self.enabled = True + + if log: + self._log = log + else: + import sys + self._log = logging.getLogger('CSSUTILS') + hdlr = logging.StreamHandler(sys.stderr) + formatter = logging.Formatter('%(levelname)s\t%(message)s') + hdlr.setFormatter(formatter) + self._log.addHandler(hdlr) + self._log.setLevel(defaultloglevel) + + self.raiseExceptions = raiseExceptions + + def __getattr__(self, name): + "use self._log items" + calls = ('debug', 'info', 'warn', 'error', 'critical', 'fatal') + other = ('setLevel', 'getEffectiveLevel', 'addHandler', 'removeHandler') + + if name in calls: + self._logcall = getattr(self._log, name) + return self.__handle + elif name in other: + return getattr(self._log, name) + else: + raise AttributeError( + '(errorhandler) No Attribute %r found' % name) + + def __handle(self, msg=u'', token=None, error=xml.dom.SyntaxErr, + neverraise=False, args=None): + """ + handles all calls + logs or raises exception + """ + if self.enabled: + if error is None: + error = xml.dom.SyntaxErr + + line, col = None, None + if token: + if isinstance(token, tuple): + value, line, col = token[1], token[2], token[3] + else: + value, line, col = token.value, token.line, token.col + msg = u'%s [%s:%s: %s]' % ( + msg, line, col, value) + + if error and self.raiseExceptions and not neverraise: + if isinstance(error, urllib2.HTTPError) or isinstance(error, urllib2.URLError): + raise + elif issubclass(error, xml.dom.DOMException): + error.line = line + error.col = col + raise error(msg) + else: + self._logcall(msg) + + def setLog(self, log): + """set log of errorhandler's log""" + self._log = log + + +class ErrorHandler(_ErrorHandler): + "Singleton, see _ErrorHandler" + instance = None + + def __init__(self, + log=None, defaultloglevel=logging.INFO, raiseExceptions=True): + + if ErrorHandler.instance is None: + ErrorHandler.instance = _ErrorHandler(log=log, + defaultloglevel=defaultloglevel, + raiseExceptions=raiseExceptions) + self.__dict__ = ErrorHandler.instance.__dict__ diff --git a/libs/cssutils/helper.py b/libs/cssutils/helper.py new file mode 100755 index 00000000..b3c55ff3 --- /dev/null +++ b/libs/cssutils/helper.py @@ -0,0 +1,137 @@ +"""cssutils helper +""" +__docformat__ = 'restructuredtext' +__version__ = '$Id: errorhandler.py 1234 2008-05-22 20:26:12Z cthedot $' + +import os +import re +import sys +import urllib + +class Deprecated(object): + """This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. + + It accepts a single paramter ``msg`` which is shown with the warning. + It should contain information which function or method to use instead. + """ + def __init__(self, msg): + self.msg = msg + + def __call__(self, func): + def newFunc(*args, **kwargs): + import warnings + warnings.warn("Call to deprecated method %r. %s" % + (func.__name__, self.msg), + category=DeprecationWarning, + stacklevel=2) + return func(*args, **kwargs) + newFunc.__name__ = func.__name__ + newFunc.__doc__ = func.__doc__ + newFunc.__dict__.update(func.__dict__) + return newFunc + +# simple escapes, all non unicodes +_simpleescapes = re.compile(ur'(\\[^0-9a-fA-F])').sub +def normalize(x): + """ + normalizes x, namely: + + - remove any \ before non unicode sequences (0-9a-zA-Z) so for + x=="c\olor\" return "color" (unicode escape sequences should have + been resolved by the tokenizer already) + - lowercase + """ + if x: + def removeescape(matchobj): + return matchobj.group(0)[1:] + x = _simpleescapes(removeescape, x) + return x.lower() + else: + return x + +def path2url(path): + """Return file URL of `path`""" + return u'file:' + urllib.pathname2url(os.path.abspath(path)) + +def pushtoken(token, tokens): + """Return new generator starting with token followed by all tokens in + ``tokens``""" + # TODO: may use itertools.chain? + yield token + for t in tokens: + yield t + +def string(value): + """ + Serialize value with quotes e.g.:: + + ``a \'string`` => ``'a \'string'`` + """ + # \n = 0xa, \r = 0xd, \f = 0xc + value = value.replace(u'\n', u'\\a ').replace( + u'\r', u'\\d ').replace( + u'\f', u'\\c ').replace( + u'"', u'\\"') + + if value.endswith(u'\\'): + value = value[:-1] + u'\\\\' + + return u'"%s"' % value + +def stringvalue(string): + """ + Retrieve actual value of string without quotes. Escaped + quotes inside the value are resolved, e.g.:: + + ``'a \'string'`` => ``a 'string`` + """ + return string.replace(u'\\'+string[0], string[0])[1:-1] + +_match_forbidden_in_uri = re.compile(ur'''.*?[\(\)\s\;,'"]''', re.U).match +def uri(value): + """ + Serialize value by adding ``url()`` and with quotes if needed e.g.:: + + ``"`` => ``url("\"")`` + """ + if _match_forbidden_in_uri(value): + value = string(value) + return u'url(%s)' % value + +def urivalue(uri): + """ + Return actual content without surrounding "url(" and ")" + and removed surrounding quotes too including contained + escapes of quotes, e.g.:: + + ``url("\"")`` => ``"`` + """ + uri = uri[uri.find('(')+1:-1].strip() + if uri and (uri[0] in '\'"') and (uri[0] == uri[-1]): + return stringvalue(uri) + else: + return uri + +#def normalnumber(num): +# """ +# Return normalized number as string. +# """ +# sign = '' +# if num.startswith('-'): +# sign = '-' +# num = num[1:] +# elif num.startswith('+'): +# num = num[1:] +# +# if float(num) == 0.0: +# return '0' +# else: +# if num.find('.') == -1: +# return sign + str(int(num)) +# else: +# a, b = num.split('.') +# if not a: +# a = '0' +# return '%s%s.%s' % (sign, int(a), b) diff --git a/libs/cssutils/parse.py b/libs/cssutils/parse.py new file mode 100755 index 00000000..715a320a --- /dev/null +++ b/libs/cssutils/parse.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +"""A validating CSSParser""" +__all__ = ['CSSParser'] +__docformat__ = 'restructuredtext' +__version__ = '$Id$' + +from helper import path2url +import codecs +import cssutils +import os +import sys +import tokenize2 +import urllib + +from cssutils import css + +if sys.version_info < (2,6): + bytes = str + +class CSSParser(object): + """Parse a CSS StyleSheet from URL, string or file and return a DOM Level 2 + CSS StyleSheet object. + + Usage:: + + parser = CSSParser() + # optionally + parser.setFetcher(fetcher) + sheet = parser.parseFile('test1.css', 'ascii') + print sheet.cssText + """ + def __init__(self, log=None, loglevel=None, raiseExceptions=None, + fetcher=None, parseComments=True, + validate=True): + """ + :param log: + logging object + :param loglevel: + logging loglevel + :param raiseExceptions: + if log should simply log (default) or raise errors during + parsing. Later while working with the resulting sheets + the setting used in cssutils.log.raiseExeptions is used + :param fetcher: + see ``setFetcher(fetcher)`` + :param parseComments: + if comments should be added to CSS DOM or simply omitted + :param validate: + if parsing should validate, may be overwritten in parse methods + """ + if log is not None: + cssutils.log.setLog(log) + if loglevel is not None: + cssutils.log.setLevel(loglevel) + + # remember global setting + self.__globalRaising = cssutils.log.raiseExceptions + if raiseExceptions: + self.__parseRaising = raiseExceptions + else: + # DEFAULT during parse + self.__parseRaising = False + + self.__tokenizer = tokenize2.Tokenizer(doComments=parseComments) + self.setFetcher(fetcher) + + self._validate = validate + + def __parseSetting(self, parse): + """during parse exceptions may be handled differently depending on + init parameter ``raiseExceptions`` + """ + if parse: + cssutils.log.raiseExceptions = self.__parseRaising + else: + cssutils.log.raiseExceptions = self.__globalRaising + + def parseStyle(self, cssText, encoding='utf-8', validate=None): + """Parse given `cssText` which is assumed to be the content of + a HTML style attribute. + + :param cssText: + CSS string to parse + :param encoding: + It will be used to decode `cssText` if given as a (byte) + string. + :param validate: + If given defines if validation is used. Uses CSSParser settings as + fallback + :returns: + :class:`~cssutils.css.CSSStyleDeclaration` + """ + self.__parseSetting(True) + if isinstance(cssText, bytes): + # TODO: use codecs.getdecoder('css') here? + cssText = cssText.decode(encoding) + if validate is None: + validate = self._validate + style = css.CSSStyleDeclaration(cssText, validating=validate) + self.__parseSetting(False) + return style + + def parseString(self, cssText, encoding=None, href=None, media=None, + title=None, + validate=None): + """Parse `cssText` as :class:`~cssutils.css.CSSStyleSheet`. + Errors may be raised (e.g. UnicodeDecodeError). + + :param cssText: + CSS string to parse + :param encoding: + If ``None`` the encoding will be read from BOM or an @charset + rule or defaults to UTF-8. + If given overrides any found encoding including the ones for + imported sheets. + It also will be used to decode `cssText` if given as a (byte) + string. + :param href: + The ``href`` attribute to assign to the parsed style sheet. + Used to resolve other urls in the parsed sheet like @import hrefs. + :param media: + The ``media`` attribute to assign to the parsed style sheet + (may be a MediaList, list or a string). + :param title: + The ``title`` attribute to assign to the parsed style sheet. + :param validate: + If given defines if validation is used. Uses CSSParser settings as + fallback + :returns: + :class:`~cssutils.css.CSSStyleSheet`. + """ + self.__parseSetting(True) + # TODO: py3 needs bytes here! + if isinstance(cssText, bytes): + cssText = codecs.getdecoder('css')(cssText, encoding=encoding)[0] + + if validate is None: + validate = self._validate + + sheet = cssutils.css.CSSStyleSheet(href=href, + media=cssutils.stylesheets.MediaList(media), + title=title, + validating=validate) + sheet._setFetcher(self.__fetcher) + # tokenizing this ways closes open constructs and adds EOF + sheet._setCssTextWithEncodingOverride(self.__tokenizer.tokenize(cssText, + fullsheet=True), + encodingOverride=encoding) + self.__parseSetting(False) + return sheet + + def parseFile(self, filename, encoding=None, + href=None, media=None, title=None, + validate=None): + """Retrieve content from `filename` and parse it. Errors may be raised + (e.g. IOError). + + :param filename: + of the CSS file to parse, if no `href` is given filename is + converted to a (file:) URL and set as ``href`` of resulting + stylesheet. + If `href` is given it is set as ``sheet.href``. Either way + ``sheet.href`` is used to resolve e.g. stylesheet imports via + @import rules. + :param encoding: + Value ``None`` defaults to encoding detection via BOM or an + @charset rule. + Other values override detected encoding for the sheet at + `filename` including any imported sheets. + :returns: + :class:`~cssutils.css.CSSStyleSheet`. + """ + if not href: + # prepend // for file URL, urllib does not do this? + #href = u'file:' + urllib.pathname2url(os.path.abspath(filename)) + href = path2url(filename) + + return self.parseString(open(filename, 'rb').read(), + encoding=encoding, # read returns a str + href=href, media=media, title=title, + validate=validate) + + def parseUrl(self, href, encoding=None, media=None, title=None, + validate=None): + """Retrieve content from URL `href` and parse it. Errors may be raised + (e.g. URLError). + + :param href: + URL of the CSS file to parse, will also be set as ``href`` of + resulting stylesheet + :param encoding: + Value ``None`` defaults to encoding detection via HTTP, BOM or an + @charset rule. + A value overrides detected encoding for the sheet at ``href`` + including any imported sheets. + :returns: + :class:`~cssutils.css.CSSStyleSheet`. + """ + encoding, enctype, text = cssutils.util._readUrl(href, + fetcher=self.__fetcher, + overrideEncoding=encoding) + if enctype == 5: + # do not use if defaulting to UTF-8 + encoding = None + + if text is not None: + return self.parseString(text, encoding=encoding, + href=href, media=media, title=title, + validate=validate) + + def setFetcher(self, fetcher=None): + """Replace the default URL fetch function with a custom one. + + :param fetcher: + A function which gets a single parameter + + ``url`` + the URL to read + + and must return ``(encoding, content)`` where ``encoding`` is the + HTTP charset normally given via the Content-Type header (which may + simply omit the charset in which case ``encoding`` would be + ``None``) and ``content`` being the string (or unicode) content. + + The Mimetype should be 'text/css' but this has to be checked by the + fetcher itself (the default fetcher emits a warning if encountering + a different mimetype). + + Calling ``setFetcher`` with ``fetcher=None`` resets cssutils + to use its default function. + """ + self.__fetcher = fetcher diff --git a/libs/cssutils/prodparser.py b/libs/cssutils/prodparser.py new file mode 100755 index 00000000..ac7825cc --- /dev/null +++ b/libs/cssutils/prodparser.py @@ -0,0 +1,733 @@ +# -*- coding: utf-8 -*- +"""Productions parser used by css and stylesheets classes to parse +test into a cssutils.util.Seq and at the same time retrieving +additional specific cssutils.util.Item objects for later use. + +TODO: + - ProdsParser + - handle EOF or STOP? + - handle unknown @rules + - handle S: maybe save to Seq? parameterized? + - store['_raw']: always? + + - Sequence: + - opt first(), naive impl for now + +""" +__all__ = ['ProdParser', 'Sequence', 'Choice', 'Prod', 'PreDef'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: parse.py 1418 2008-08-09 19:27:50Z cthedot $' + +from helper import pushtoken +import cssutils +import re +import string +import sys + + +class ParseError(Exception): + """Base Exception class for ProdParser (used internally).""" + pass + +class Done(ParseError): + """Raised if Sequence or Choice is finished and no more Prods left.""" + pass + +class Exhausted(ParseError): + """Raised if Sequence or Choice is finished but token is given.""" + pass + +class Missing(ParseError): + """Raised if Sequence or Choice is not finished but no matching token given.""" + pass + +class NoMatch(ParseError): + """Raised if nothing in Sequence or Choice does match.""" + pass + + +class Choice(object): + """A Choice of productions (Sequence or single Prod).""" + + def __init__(self, *prods, **options): + """ + *prods + Prod or Sequence objects + options: + optional=False + """ + self._prods = prods + + try: + self.optional = options['optional'] + except KeyError, e: + for p in self._prods: + if p.optional: + self.optional = True + break + else: + self.optional = False + + self.reset() + + def reset(self): + """Start Choice from zero""" + self._exhausted = False + + def matches(self, token): + """Check if token matches""" + for prod in self._prods: + if prod.matches(token): + return True + return False + + def nextProd(self, token): + """ + Return: + + - next matching Prod or Sequence + - ``None`` if any Prod or Sequence is optional and no token matched + - raise ParseError if nothing matches and all are mandatory + - raise Exhausted if choice already done + + ``token`` may be None but this occurs when no tokens left.""" + if not self._exhausted: + optional = False + for x in self._prods: + if x.matches(token): + self._exhausted = True + x.reset() + return x + elif x.optional: + optional = True + else: + if not optional: + # None matched but also None is optional + raise ParseError(u'No match in %s' % self) + elif token: + raise Exhausted(u'Extra token') + + def __str__(self): + return u'Choice(%s)' % u', '.join([str(x) for x in self._prods]) + + +class Sequence(object): + """A Sequence of productions (Choice or single Prod).""" + def __init__(self, *prods, **options): + """ + *prods + Prod or Sequence objects + **options: + minmax = lambda: (1, 1) + callback returning number of times this sequence may run + """ + self._prods = prods + try: + minmax = options['minmax'] + except KeyError: + minmax = lambda: (1, 1) + + self._min, self._max = minmax() + if self._max is None: + # unlimited + try: + # py2.6/3 + self._max = sys.maxsize + except AttributeError: + # py<2.6 + self._max = sys.maxint + + self._prodcount = len(self._prods) + self.reset() + + def matches(self, token): + """Called by Choice to try to find if Sequence matches.""" + for prod in self._prods: + if prod.matches(token): + return True + try: + if not prod.optional: + break + except AttributeError: + pass + return False + + def reset(self): + """Reset this Sequence if it is nested.""" + self._roundstarted = False + self._i = 0 + self._round = 0 + + def _currentName(self): + """Return current element of Sequence, used by name""" + # TODO: current impl first only if 1st if an prod! + for prod in self._prods[self._i:]: + if not prod.optional: + return str(prod) + else: + return 'Sequence' + + optional = property(lambda self: self._min == 0) + + def nextProd(self, token): + """Return + + - next matching Prod or Choice + - raises ParseError if nothing matches + - raises Exhausted if sequence already done + """ + while self._round < self._max: + # for this round + i = self._i + round = self._round + p = self._prods[i] + if i == 0: + self._roundstarted = False + + # for next round + self._i += 1 + if self._i == self._prodcount: + self._round += 1 + self._i = 0 + + if p.matches(token): + self._roundstarted = True + # reset nested Choice or Prod to use from start + p.reset() + return p + + elif p.optional: + continue + + elif round < self._min: + raise Missing(u'Missing token for production %s' % p) + + elif not token: + if self._roundstarted: + raise Missing(u'Missing token for production %s' % p) + else: + raise Done() + + else: + raise NoMatch(u'No matching production for token') + + if token: + raise Exhausted(u'Extra token') + + def __str__(self): + return u'Sequence(%s)' % u', '.join([str(x) for x in self._prods]) + + +class Prod(object): + """Single Prod in Sequence or Choice.""" + def __init__(self, name, match, optional=False, + toSeq=None, toStore=None, + stop=False, stopAndKeep=False, + nextSor=False, mayEnd=False, + storeToken=None, + exception=None): + """ + name + name used for error reporting + match callback + function called with parameters tokentype and tokenvalue + returning True, False or raising ParseError + toSeq callback (optional) or False + calling toSeq(token, tokens) returns (type_, val) == (token[0], token[1]) + to be appended to seq else simply unaltered (type_, val) + + if False nothing is added + + toStore (optional) + key to save util.Item to store or callback(store, util.Item) + optional = False + wether Prod is optional or not + stop = False + if True stop parsing of tokens here + stopAndKeep + if True stop parsing of tokens here but return stopping + token in unused tokens + nextSor=False + next is S or other like , or / (CSSValue) + mayEnd = False + no token must follow even defined by Sequence. + Used for operator ',/ ' currently only + + storeToken = None + if True toStore saves simple token tuple and not and Item object + to store. Old style processing, TODO: resolve + + exception = None + exception to be raised in case of error, normaly SyntaxErr + """ + self._name = name + self.match = match + self.optional = optional + self.stop = stop + self.stopAndKeep = stopAndKeep + self.nextSor = nextSor + self.mayEnd = mayEnd + self.storeToken = storeToken + self.exception = exception + + def makeToStore(key): + "Return a function used by toStore." + def toStore(store, item): + "Set or append store item." + if key in store: + _v = store[key] + if not isinstance(_v, list): + store[key] = [_v] + store[key].append(item) + else: + store[key] = item + return toStore + + if toSeq or toSeq is False: + # called: seq.append(toSeq(value)) + self.toSeq = toSeq + else: + self.toSeq = lambda t, tokens: (t[0], t[1]) + + if hasattr(toStore, '__call__'): + self.toStore = toStore + elif toStore: + self.toStore = makeToStore(toStore) + else: + # always set! + self.toStore = None + + def matches(self, token): + """Return if token matches.""" + if not token: + return False + type_, val, line, col = token + return self.match(type_, val) + + def reset(self): + pass + + def __str__(self): + return self._name + + def __repr__(self): + return "" % ( + self.__class__.__name__, self._name, id(self)) + + +# global tokenizer as there is only one! +tokenizer = cssutils.tokenize2.Tokenizer() + +class ProdParser(object): + """Productions parser.""" + def __init__(self, clear=True): + self.types = cssutils.cssproductions.CSSProductions + self._log = cssutils.log + if clear: + tokenizer.clear() + + def _texttotokens(self, text): + """Build a generator which is the only thing that is parsed! + old classes may use lists etc + """ + if isinstance(text, basestring): + # DEFAULT, to tokenize strip space + return tokenizer.tokenize(text.strip()) + + elif isinstance(text, tuple): + # OLD: (token, tokens) or a single token + if len(text) == 2: + # (token, tokens) + chain([token], tokens) + else: + # single token + return iter([text]) + + elif isinstance(text, list): + # OLD: generator from list + return iter(text) + + else: + # DEFAULT, already tokenized, assume generator + return text + + def _SorTokens(self, tokens, until=',/'): + """New tokens generator which has S tokens removed, + if followed by anything in ``until``, normally a ``,``.""" + for token in tokens: + if token[0] == self.types.S: + try: + next_ = tokens.next() + except StopIteration: + yield token + else: + if next_[1] in until: + # omit S as e.g. ``,`` has been found + yield next_ + elif next_[0] == self.types.COMMENT: + # pass COMMENT + yield next_ + else: + yield token + yield next_ + + elif token[0] == self.types.COMMENT: + # pass COMMENT + yield token + else: + yield token + break + # normal mode again + for token in tokens: + yield token + + + def parse(self, text, name, productions, keepS=False, store=None): + """ + text (or token generator) + to parse, will be tokenized if not a generator yet + + may be: + - a string to be tokenized + - a single token, a tuple + - a tuple of (token, tokensGenerator) + - already tokenized so a tokens generator + + name + used for logging + productions + used to parse tokens + keepS + if WS should be added to Seq or just be ignored + store UPDATED + If a Prod defines ``toStore`` the key defined there + is a key in store to be set or if store[key] is a list + the next Item is appended here. + + TODO: NEEDED? : + Key ``raw`` is always added and holds all unprocessed + values found + + returns + :wellformed: True or False + :seq: a filled cssutils.util.Seq object which is NOT readonly yet + :store: filled keys defined by Prod.toStore + :unusedtokens: token generator containing tokens not used yet + """ + tokens = self._texttotokens(text) + if not tokens: + self._log.error(u'No content to parse.') + # TODO: return??? + + seq = cssutils.util.Seq(readonly=False) + if not store: # store for specific values + store = {} + prods = [productions] # stack of productions + wellformed = True + + # while no real token is found any S are ignored + started = False + stopall = False + prod = None + # flag if default S handling should be done + defaultS = True + while True: + try: + token = tokens.next() + except StopIteration: + break + type_, val, line, col = token + + # default productions + if type_ == self.types.COMMENT: + # always append COMMENT + seq.append(cssutils.css.CSSComment(val), + cssutils.css.CSSComment, line, col) + elif defaultS and type_ == self.types.S: + # append S (but ignore starting ones) + if not keepS or not started: + continue + else: + seq.append(val, type_, line, col) +# elif type_ == self.types.ATKEYWORD: +# # @rule +# r = cssutils.css.CSSUnknownRule(cssText=val) +# seq.append(r, type(r), line, col) + elif type_ == self.types.INVALID: + # invalidate parse + wellformed = False + self._log.error(u'Invalid token: %r' % (token,)) + break + elif type_ == 'EOF': + # do nothing? (self.types.EOF == True!) + pass + else: + started = True # check S now + nextSor = False # reset + + try: + while True: + # find next matching production + try: + prod = prods[-1].nextProd(token) + except (Exhausted, NoMatch), e: + # try next + prod = None + if isinstance(prod, Prod): + # found actual Prod, not a Choice or Sequence + break + elif prod: + # nested Sequence, Choice + prods.append(prod) + else: + # nested exhausted, try in parent + if len(prods) > 1: + prods.pop() + else: + raise ParseError('No match') + except ParseError, e: + wellformed = False + self._log.error(u'%s: %s: %r' % (name, e, token)) + break + else: + # process prod + if prod.toSeq and not prod.stopAndKeep: + type_, val = prod.toSeq(token, tokens) + if val is not None: + seq.append(val, type_, line, col) + if prod.toStore: + if not prod.storeToken: + prod.toStore(store, seq[-1]) + else: + # workaround for now for old style token + # parsing! + # TODO: remove when all new style + prod.toStore(store, token) + + if prod.stop: # EOF? + # stop here and ignore following tokens + break + + if prod.stopAndKeep: # e.g. ; + # stop here and ignore following tokens + # but keep this token for next run + tokenizer.push(token) + stopall = True + break + + if prod.nextSor: + # following is S or other token (e.g. ",")? + # remove S if + tokens = self._SorTokens(tokens, ',/') + defaultS = False + else: + defaultS = True + + lastprod = prod + + if not stopall: + # stop immediately + while True: + # all productions exhausted? + try: + prod = prods[-1].nextProd(token=None) + except Done, e: + # ok + prod = None + + except Missing, e: + prod = None + # last was a S operator which may End a Sequence, then ok + if hasattr(lastprod, 'mayEnd') and not lastprod.mayEnd: + wellformed = False + self._log.error(u'%s: %s' % (name, e)) + + except ParseError, e: + prod = None + wellformed = False + self._log.error(u'%s: %s' % (name, e)) + + else: + if prods[-1].optional: + prod = None + elif prod and prod.optional: + # ignore optional + continue + + if prod and not prod.optional: + wellformed = False + self._log.error(u'%s: Missing token for production %r' + % (name, str(prod))) + break + elif len(prods) > 1: + # nested exhausted, next in parent + prods.pop() + else: + break + + # trim S from end + seq.rstrip() + return wellformed, seq, store, tokens + + +class PreDef(object): + """Predefined Prod definition for use in productions definition + for ProdParser instances. + """ + types = cssutils.cssproductions.CSSProductions + reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$') + + @staticmethod + def calc(toSeq=None, nextSor=False): + return Prod(name=u'calcfunction', + match=lambda t, v: u'calc(' == cssutils.helper.normalize(v), + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def char(name='char', char=u',', toSeq=None, + stop=False, stopAndKeep=False, + optional=True, nextSor=False): + "any CHAR" + return Prod(name=name, match=lambda t, v: v == char, toSeq=toSeq, + stop=stop, stopAndKeep=stopAndKeep, optional=optional, + nextSor=nextSor) + + @staticmethod + def comma(): + return PreDef.char(u'comma', u',') + + @staticmethod + def dimension(nextSor=False, stop=False): + return Prod(name=u'dimension', + match=lambda t, v: t == PreDef.types.DIMENSION, + toSeq=lambda t, tokens: (t[0], cssutils.helper.normalize(t[1])), + stop=stop, + nextSor=nextSor) + + @staticmethod + def function(toSeq=None, nextSor=False): + return Prod(name=u'function', + match=lambda t, v: t == PreDef.types.FUNCTION, + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def funcEnd(stop=False): + ")" + return PreDef.char(u'end FUNC ")"', u')', + stop=stop) + + @staticmethod + def hexcolor(stop=False, nextSor=False): + "#123 or #123456" + return Prod(name='HEX color', + match=lambda t, v: ( + t == PreDef.types.HASH and + PreDef.reHexcolor.match(v) + ), + stop=stop, + nextSor=nextSor) + + @staticmethod + def ident(stop=False, toStore=None, nextSor=False): + return Prod(name=u'ident', + match=lambda t, v: t == PreDef.types.IDENT, + stop=stop, + toStore=toStore, + nextSor=nextSor) + + @staticmethod + def number(stop=False, toSeq=None, nextSor=False): + return Prod(name=u'number', + match=lambda t, v: t == PreDef.types.NUMBER, + stop=stop, + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def percentage(stop=False, toSeq=None, nextSor=False): + return Prod(name=u'percentage', + match=lambda t, v: t == PreDef.types.PERCENTAGE, + stop=stop, + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def string(stop=False, nextSor=False): + "string delimiters are removed by default" + return Prod(name=u'string', + match=lambda t, v: t == PreDef.types.STRING, + toSeq=lambda t, tokens: (t[0], cssutils.helper.stringvalue(t[1])), + stop=stop, + nextSor=nextSor) + + @staticmethod + def S(name=u'whitespace', toSeq=None, optional=False): + return Prod(name=name, + match=lambda t, v: t == PreDef.types.S, + toSeq=toSeq, + optional=optional, + mayEnd=True) + + @staticmethod + def unary(stop=False, toSeq=None, nextSor=False): + "+ or -" + return Prod(name=u'unary +-', match=lambda t, v: v in (u'+', u'-'), + optional=True, + stop=stop, + toSeq=toSeq, + nextSor=nextSor) + + @staticmethod + def uri(stop=False, nextSor=False): + "'url(' and ')' are removed and URI is stripped" + return Prod(name=u'URI', + match=lambda t, v: t == PreDef.types.URI, + toSeq=lambda t, tokens: (t[0], cssutils.helper.urivalue(t[1])), + stop=stop, + nextSor=nextSor) + + @staticmethod + def unicode_range(stop=False, nextSor=False): + "u+123456-abc normalized to lower `u`" + return Prod(name='unicode-range', + match=lambda t, v: t == PreDef.types.UNICODE_RANGE, + toSeq=lambda t, tokens: (t[0], t[1].lower()), + stop=stop, + nextSor=nextSor + ) + + @staticmethod + def variable(toSeq=None, stop=False, nextSor=False): + return Prod(name=u'variable', + match=lambda t, v: u'var(' == cssutils.helper.normalize(v), + toSeq=toSeq, + stop=stop, + nextSor=nextSor) + + # used for MarginRule for now: + @staticmethod + def unknownrule(name=u'@', toStore=None): + """@rule dummy (matches ATKEYWORD to remove unknown rule tokens from + stream:: + + @x; + @x {...} + + no nested yet! + """ + def rule(tokens): + saved = [] + for t in tokens: + saved.append(t) + if (t[1] == u'}' or t[1] == u';'): + return cssutils.css.CSSUnknownRule(saved) + + return Prod(name=name, + match=lambda t, v: t == u'ATKEYWORD', + toSeq=lambda t, tokens: (u'CSSUnknownRule', + rule(pushtoken(t, tokens)) + ), + toStore=toStore + ) diff --git a/libs/cssutils/profiles.py b/libs/cssutils/profiles.py new file mode 100755 index 00000000..57c63bd4 --- /dev/null +++ b/libs/cssutils/profiles.py @@ -0,0 +1,791 @@ +"""CSS profiles. + +Profiles is based on code by Kevin D. Smith, orginally used as cssvalues, +thanks! +""" +__all__ = ['Profiles'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: cssproperties.py 1116 2008-03-05 13:52:23Z cthedot $' + +import re +import types + +class NoSuchProfileException(Exception): + """Raised if no profile with given name is found""" + pass + + +# dummies, replaced in Profiles.addProfile +_fontRegexReplacements = { + '__FONT_FAMILY_SINGLE': lambda f: False, + '__FONT_WITH_1_FAMILY': lambda f: False + } + +def _fontFamilyValidator(families): + """Check if ``font-family`` value is valid, regex is too slow. + + Splits on ``,`` and checks each family separately. + Somehow naive as font-family name could contain a "," but this is unlikely. + Still should be a TODO. + """ + match = _fontRegexReplacements['__FONT_FAMILY_SINGLE'] + + for f in families.split(u','): + if not match(f.strip()): + return False + return True + +def _fontValidator(font): + """Check if font value is valid, regex is too slow. + + Checks everything before ``,`` on basic font value. Everything after should + be a valid font-family value. + """ + if u',' in font: + # split off until 1st family + font1, families2 = font.split(u',', 1) + else: + font1, families2 = font, None + + if not _fontRegexReplacements['__FONT_WITH_1_FAMILY'](font1.strip()): + return False + + if families2 and not _fontFamilyValidator(families2): + return False + + return True + + +class Profiles(object): + """ + All profiles used for validation. ``cssutils.profile`` is a + preset object of this class and used by all properties for validation. + + Predefined profiles are (use + :meth:`~cssutils.profiles.Profiles.propertiesByProfile` to + get a list of defined properties): + + :attr:`~cssutils.profiles.Profiles.CSS_LEVEL_2` + Properties defined by CSS2.1 + :attr:`~cssutils.profiles.Profiles.CSS3_BASIC_USER_INTERFACE` + Currently resize and outline properties only + :attr:`~cssutils.profiles.Profiles.CSS3_BOX` + Currently overflow related properties only + :attr:`~cssutils.profiles.Profiles.CSS3_COLOR` + CSS 3 color properties + :attr:`~cssutils.profiles.Profiles.CSS3_PAGED_MEDIA` + As defined at http://www.w3.org/TR/css3-page/ (at 090307) + + Predefined macros are: + + :attr:`~cssutils.profiles.Profiles._TOKEN_MACROS` + Macros containing the token values as defined to CSS2 + :attr:`~cssutils.profiles.Profiles._MACROS` + Additional general macros. + + If you want to redefine any of these macros do this in your custom + macros. + """ + CSS_LEVEL_2 = u'CSS Level 2.1' + CSS3_BACKGROUNDS_AND_BORDERS = u'CSS Backgrounds and Borders Module Level 3' + CSS3_BASIC_USER_INTERFACE = u'CSS3 Basic User Interface Module' + CSS3_BOX = CSS_BOX_LEVEL_3 = u'CSS Box Module Level 3' + CSS3_COLOR = CSS_COLOR_LEVEL_3 = u'CSS Color Module Level 3' + CSS3_FONTS = u'CSS Fonts Module Level 3' + CSS3_FONT_FACE = u'CSS Fonts Module Level 3 @font-face properties' + CSS3_PAGED_MEDIA = u'CSS3 Paged Media Module' + CSS3_TEXT = u'CSS Text Level 3' + + _TOKEN_MACROS = { + 'ident': r'[-]?{nmstart}{nmchar}*', + 'name': r'{nmchar}+', + 'nmstart': r'[_a-z]|{nonascii}|{escape}', + 'nonascii': r'[^\0-\177]', + 'unicode': r'\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?', + 'escape': r'{unicode}|\\[ -~\200-\777]', + # 'escape': r'{unicode}|\\[ -~\200-\4177777]', + 'int': r'[-]?\d+', + 'nmchar': r'[\w-]|{nonascii}|{escape}', + 'num': r'[-]?\d+|[-]?\d*\.\d+', + 'positivenum': r'\d+|\d*\.\d+', + 'number': r'{num}', + 'string': r'{string1}|{string2}', + 'string1': r'"(\\\"|[^\"])*"', + 'uri': r'url\({w}({string}|(\\\)|[^\)])+){w}\)', + 'string2': r"'(\\\'|[^\'])*'", + 'nl': r'\n|\r\n|\r|\f', + 'w': r'\s*', + } + _MACROS = { + 'hexcolor': r'#[0-9a-f]{3}|#[0-9a-f]{6}', + 'rgbcolor': r'rgb\({w}{int}{w}\,{w}{int}{w}\,{w}{int}{w}\)|rgb\({w}{num}%{w}\,{w}{num}%{w}\,{w}{num}%{w}\)', + 'namedcolor': r'(transparent|orange|maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray)', + 'uicolor': r'(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)', + 'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{uicolor}', + #'color': r'(maroon|red|orange|yellow|olive|purple|fuchsia|white|lime|green|navy|blue|aqua|teal|black|silver|gray|ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)|#[0-9a-f]{3}|#[0-9a-f]{6}|rgb\({w}{int}{w},{w}{int}{w},{w}{int}{w}\)|rgb\({w}{num}%{w},{w}{num}%{w},{w}{num}%{w}\)', + 'integer': r'{int}', + 'length': r'0|{num}(em|ex|px|in|cm|mm|pt|pc)', + 'positivelength': r'0|{positivenum}(em|ex|px|in|cm|mm|pt|pc)', + 'angle': r'0|{num}(deg|grad|rad)', + 'time': r'0|{num}m?s', + 'frequency': r'0|{num}k?Hz', + 'percentage': r'{num}%', + 'shadow': '(inset)?{w}{length}{w}{length}{w}{length}?{w}{length}?{w}{color}?' + } + + def __init__(self, log=None): + """A few profiles are predefined.""" + self._log = log + + # macro cache + self._usedMacros = Profiles._TOKEN_MACROS.copy() + self._usedMacros.update(Profiles._MACROS.copy()) + + # to keep order, REFACTOR! + self._profileNames = [] + # for reset if macro changes + self._rawProfiles = {} + # already compiled profiles: {profile: {property: checkfunc, ...}, ...} + self._profilesProperties = {} + + self._defaultProfiles = None + + self.addProfiles([(self.CSS_LEVEL_2, + properties[self.CSS_LEVEL_2], + macros[self.CSS_LEVEL_2] + ), + (self.CSS3_BACKGROUNDS_AND_BORDERS, + properties[self.CSS3_BACKGROUNDS_AND_BORDERS], + macros[self.CSS3_BACKGROUNDS_AND_BORDERS] + ), + (self.CSS3_BASIC_USER_INTERFACE, + properties[self.CSS3_BASIC_USER_INTERFACE], + macros[self.CSS3_BASIC_USER_INTERFACE] + ), + (self.CSS3_BOX, + properties[self.CSS3_BOX], + macros[self.CSS3_BOX] + ), + (self.CSS3_COLOR, + properties[self.CSS3_COLOR], + macros[self.CSS3_COLOR] + ), + (self.CSS3_FONTS, + properties[self.CSS3_FONTS], + macros[self.CSS3_FONTS] + ), + # new object for font-face only? + (self.CSS3_FONT_FACE, + properties[self.CSS3_FONT_FACE], + macros[self.CSS3_FONTS] + ), + (self.CSS3_PAGED_MEDIA, + properties[self.CSS3_PAGED_MEDIA], + macros[self.CSS3_PAGED_MEDIA] + ), + (self.CSS3_TEXT, + properties[self.CSS3_TEXT], + macros[self.CSS3_TEXT] + ) + ]) + + self.__update_knownNames() + + def _expand_macros(self, dictionary, macros): + """Expand macros in token dictionary""" + def macro_value(m): + return '(?:%s)' % macros[m.groupdict()['macro']] + + for key, value in dictionary.items(): + if not hasattr(value, '__call__'): + while re.search(r'{[a-z][a-z0-9-]*}', value): + value = re.sub(r'{(?P[a-z][a-z0-9-]*)}', + macro_value, value) + dictionary[key] = value + + return dictionary + + def _compile_regexes(self, dictionary): + """Compile all regular expressions into callable objects""" + for key, value in dictionary.items(): + # might be a function (font-family) as regex is too slow + if not hasattr(value, '__call__') and not isinstance(value, + types.FunctionType): + value = re.compile('^(?:%s)$' % value, re.I).match + dictionary[key] = value + + return dictionary + + def __update_knownNames(self): + self._knownNames = [] + for properties in self._profilesProperties.values(): + self._knownNames.extend(properties.keys()) + + def _getDefaultProfiles(self): + "If not explicitly set same as Profiles.profiles but in reverse order." + if not self._defaultProfiles: + return self.profiles + else: + return self._defaultProfiles + + def _setDefaultProfiles(self, profiles): + "profiles may be a single or a list of profile names" + if isinstance(profiles, basestring): + self._defaultProfiles = (profiles,) + else: + self._defaultProfiles = profiles + + defaultProfiles = property(_getDefaultProfiles, + _setDefaultProfiles, + doc=u"Names of profiles to use for validation." + u"To use e.g. the CSS2 profile set " + u"``cssutils.profile.defaultProfiles = " + u"cssutils.profile.CSS_LEVEL_2``") + + profiles = property(lambda self: self._profileNames, + doc=u'Names of all profiles in order as defined.') + + knownNames = property(lambda self: self._knownNames, + doc="All known property names of all profiles.") + + def _resetProperties(self, newMacros=None): + "reset all props from raw values as changes in macros happened" + # base + macros = Profiles._TOKEN_MACROS.copy() + macros.update(Profiles._MACROS.copy()) + + # former + for profile in self._profileNames: + macros.update(self._rawProfiles[profile]['macros']) + + # new + if newMacros: + macros.update(newMacros) + + # reset properties + self._profilesProperties.clear() + for profile in self._profileNames: + properties = self._expand_macros( + # keep raw + self._rawProfiles[profile]['properties'].copy(), + macros) + self._profilesProperties[profile] = self._compile_regexes(properties) + + # save + self._usedMacros = macros + + + def addProfiles(self, profiles): + """Add a list of profiles at once. Useful as if profiles define custom + macros these are used in one go. Using `addProfile` instead my be + **very** slow instead. + """ + # add macros + for profile, properties, macros in profiles: + if macros: + self._usedMacros.update(macros) + self._rawProfiles[profile] = {'macros': macros.copy()} + + # only add new properties + for profile, properties, macros in profiles: + self.addProfile(profile, properties.copy(), None) + + + def addProfile(self, profile, properties, macros=None): + """Add a new profile with name `profile` (e.g. 'CSS level 2') + and the given `properties`. + + :param profile: + the new `profile`'s name + :param properties: + a dictionary of ``{ property-name: propery-value }`` items where + property-value is a regex which may use macros defined in given + ``macros`` or the standard macros Profiles.tokens and + Profiles.generalvalues. + + ``propery-value`` may also be a function which takes a single + argument which is the value to validate and which should return + True or False. + Any exceptions which may be raised during this custom validation + are reported or raised as all other cssutils exceptions depending + on cssutils.log.raiseExceptions which e.g during parsing normally + is False so the exceptions would be logged only. + :param macros: + may be used in the given properties definitions. There are some + predefined basic macros which may always be used in + :attr:`Profiles._TOKEN_MACROS` and :attr:`Profiles._MACROS`. + """ + if macros: + # check if known macros would change and if yes reset properties + if len(set(macros.keys()).intersection(self._usedMacros.keys())): + self._resetProperties(newMacros=macros) + + else: + # no replacement, simply continue + self._usedMacros.update(macros) + + else: + # might have been set by addProfiles before + try: + macros = self._rawProfiles[profile]['macros'] + except KeyError, e: + macros = {} + + # save name and raw props/macros if macros change to completely reset + self._profileNames.append(profile) + self._rawProfiles[profile] = {'properties': properties.copy(), + 'macros': macros.copy()} + # prepare and save properties + properties = self._expand_macros(properties, self._usedMacros) + self._profilesProperties[profile] = self._compile_regexes(properties) + + self.__update_knownNames() + + # hack for font and font-family which are too slow with regexes + if '__FONT_WITH_1_FAMILY' in properties: + _fontRegexReplacements['__FONT_WITH_1_FAMILY'] = properties['__FONT_WITH_1_FAMILY'] + if '__FONT_FAMILY_SINGLE' in properties: + _fontRegexReplacements['__FONT_FAMILY_SINGLE'] = properties['__FONT_FAMILY_SINGLE'] + + + def removeProfile(self, profile=None, all=False): + """Remove `profile` or remove `all` profiles. + + If the removed profile used custom macros all remaining profiles + are reset to reflect the macro changes. This may be quite an expensive + operation! + + :param profile: + profile name to remove + :param all: + if ``True`` removes all profiles to start with a clean state + :exceptions: + - :exc:`cssutils.profiles.NoSuchProfileException`: + If given `profile` cannot be found. + """ + if all: + self._profilesProperties.clear() + self._rawProfiles.clear() + del self._profileNames[:] + else: + reset = False + + try: + if (self._rawProfiles[profile]['macros']): + reset = True + + del self._profilesProperties[profile] + del self._rawProfiles[profile] + del self._profileNames[self._profileNames.index(profile)] + except KeyError: + raise NoSuchProfileException(u'No profile %r.' % profile) + + else: + if reset: + # reset properties as macros were removed + self._resetProperties() + + self.__update_knownNames() + + def propertiesByProfile(self, profiles=None): + """Generator: Yield property names, if no `profiles` is given all + profile's properties are used. + + :param profiles: + a single profile name or a list of names. + """ + if not profiles: + profiles = self.profiles + elif isinstance(profiles, basestring): + profiles = (profiles, ) + try: + for profile in sorted(profiles): + for name in sorted(self._profilesProperties[profile].keys()): + yield name + except KeyError, e: + raise NoSuchProfileException(e) + + def validate(self, name, value): + """Check if `value` is valid for given property `name` using **any** + profile. + + :param name: + a property name + :param value: + a CSS value (string) + :returns: + if the `value` is valid for the given property `name` in any + profile + """ + for profile in self.profiles: + if name in self._profilesProperties[profile]: + try: + # custom validation errors are caught + r = bool(self._profilesProperties[profile][name](value)) + except Exception, e: + # TODO: more specific exception? + # Validate should not be fatal though! + self._log.error(e, error=Exception) + r = False + if r: + return r + return False + + def validateWithProfile(self, name, value, profiles=None): + """Check if `value` is valid for given property `name` returning + ``(valid, profile)``. + + :param name: + a property name + :param value: + a CSS value (string) + :param profiles: + internal parameter used by Property.validate only + :returns: + ``valid, matching, profiles`` where ``valid`` is if the `value` + is valid for the given property `name` in any profile, + ``matching==True`` if it is valid in the given `profiles` + and ``profiles`` the profile names for which the value is valid + (or ``[]`` if not valid at all) + + Example:: + + >>> cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2 + >>> print cssutils.profile.validateWithProfile('color', 'rgba(1,1,1,1)') + (True, False, Profiles.CSS3_COLOR) + """ + if name not in self.knownNames: + return False, False, [] + else: + if not profiles: + profiles = self.defaultProfiles + elif isinstance(profiles, basestring): + profiles = (profiles, ) + for profilename in reversed(profiles): + # check given profiles + if name in self._profilesProperties[profilename]: + validate = self._profilesProperties[profilename][name] + try: + if validate(value): + return True, True, [profilename] + except Exception, e: + self._log.error(e, error=Exception) + + for profilename in (p for p in self._profileNames + if p not in profiles): + # check remaining profiles as well + if name in self._profilesProperties[profilename]: + validate = self._profilesProperties[profilename][name] + try: + if validate(value): + return True, False, [profilename] + except Exception, e: + self._log.error(e, error=Exception) + + names = [] + for profilename, properties in self._profilesProperties.items(): + # return profile to which name belongs + if name in properties.keys(): + names.append(profilename) + names.sort() + return False, False, names + + +properties = {} +macros = {} + + +""" +Define some regular expression fragments that will be used as +macros within the CSS property value regular expressions. +""" +macros[Profiles.CSS_LEVEL_2] = { + 'background-color': r'{color}|transparent|inherit', + 'background-image': r'{uri}|none|inherit', + #'background-position': r'({percentage}|{length})(\s*({percentage}|{length}))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?)|inherit', + 'background-position': r'({percentage}|{length}|left|center|right)(\s*({percentage}|{length}|top|center|bottom))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?)|inherit', + 'background-repeat': r'repeat|repeat-x|repeat-y|no-repeat|inherit', + 'background-attachment': r'scroll|fixed|inherit', + 'shape': r'rect\(({w}({length}|auto}){w},){3}{w}({length}|auto){w}\)', + 'counter': r'counter\({w}{ident}{w}(?:,{w}{list-style-type}{w})?\)', + 'identifier': r'{ident}', + 'family-name': r'{string}|{ident}({w}{ident})*', + 'generic-family': r'serif|sans-serif|cursive|fantasy|monospace', + 'absolute-size': r'(x?x-)?(small|large)|medium', + 'relative-size': r'smaller|larger', + + #[[ | ] [, | ]* ] | inherit + #'font-family': r'(({family-name}|{generic-family})({w},{w}({family-name}|{generic-family}))*)|inherit', + # EXTREMELY SLOW REGEX + #'font-family': r'({family-name}({w},{w}{family-name})*)|inherit', + + 'font-size': r'{absolute-size}|{relative-size}|{positivelength}|{percentage}|inherit', + 'font-style': r'normal|italic|oblique|inherit', + 'font-variant': r'normal|small-caps|inherit', + 'font-weight': r'normal|bold|bolder|lighter|[1-9]00|inherit', + 'line-height': r'normal|{number}|{length}|{percentage}|inherit', + 'list-style-image': r'{uri}|none|inherit', + 'list-style-position': r'inside|outside|inherit', + 'list-style-type': r'disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-(latin|alpha)|upper-(latin|alpha)|armenian|georgian|none|inherit', + 'margin-width': r'{length}|{percentage}|auto', + 'padding-width': r'{length}|{percentage}', + 'specific-voice': r'{ident}', + 'generic-voice': r'male|female|child', + 'content': r'{string}|{uri}|{counter}|attr\({w}{ident}{w}\)|open-quote|close-quote|no-open-quote|no-close-quote', + 'background-attrs': r'{background-color}|{background-image}|{background-repeat}|{background-attachment}|{background-position}', + 'list-attrs': r'{list-style-type}|{list-style-position}|{list-style-image}', + 'font-attrs': r'{font-style}|{font-variant}|{font-weight}', + 'text-attrs': r'underline|overline|line-through|blink', + 'overflow': r'visible|hidden|scroll|auto|inherit', +} + +""" +Define the regular expressions for validation all CSS values +""" +properties[Profiles.CSS_LEVEL_2] = { + 'azimuth': r'{angle}|(behind\s+)?(left-side|far-left|left|center-left|center|center-right|right|far-right|right-side)(\s+behind)?|behind|leftwards|rightwards|inherit', + 'background-attachment': r'{background-attachment}', + 'background-color': r'{background-color}', + 'background-image': r'{background-image}', + 'background-position': r'{background-position}', + 'background-repeat': r'{background-repeat}', + # Each piece should only be allowed one time + 'background': r'{background-attrs}(\s+{background-attrs})*|inherit', + 'border-collapse': r'collapse|separate|inherit', + 'border-spacing': r'{length}(\s+{length})?|inherit', + 'bottom': r'{length}|{percentage}|auto|inherit', + 'caption-side': r'top|bottom|inherit', + 'clear': r'none|left|right|both|inherit', + 'clip': r'{shape}|auto|inherit', + 'color': r'{color}|inherit', + 'content': r'none|normal|{content}(\s+{content})*|inherit', + 'counter-increment': r'({ident}(\s+{integer})?)(\s+({ident}(\s+{integer})?))*|none|inherit', + 'counter-reset': r'({ident}(\s+{integer})?)(\s+({ident}(\s+{integer})?))*|none|inherit', + 'cue-after': r'{uri}|none|inherit', + 'cue-before': r'{uri}|none|inherit', + 'cue': r'({uri}|none|inherit){1,2}|inherit', + #'cursor': r'((({uri}{w},{w})*)?(auto|crosshair|default|pointer|move|(e|ne|nw|n|se|sw|s|w)-resize|text|wait|help|progress))|inherit', + 'direction': r'ltr|rtl|inherit', + 'display': r'inline|block|list-item|run-in|inline-block|table|inline-table|table-row-group|table-header-group|table-footer-group|table-row|table-column-group|table-column|table-cell|table-caption|none|inherit', + 'elevation': r'{angle}|below|level|above|higher|lower|inherit', + 'empty-cells': r'show|hide|inherit', + 'float': r'left|right|none|inherit', + + # regex too slow: + # 'font-family': r'{font-family}', + 'font-family': _fontFamilyValidator, + '__FONT_FAMILY_SINGLE': r'{family-name}', + + 'font-size': r'{font-size}', + 'font-style': r'{font-style}', + 'font-variant': r'{font-variant}', + 'font-weight': r'{font-weight}', + + # regex too slow and wrong too: + # 'font': r'({font-attrs}\s+)*{font-size}({w}/{w}{line-height})?\s+{font-family}|caption|icon|menu|message-box|small-caption|status-bar|inherit', + 'font': _fontValidator, + '__FONT_WITH_1_FAMILY': r'(({font-attrs}\s+)*{font-size}({w}/{w}{line-height})?\s+{family-name})|caption|icon|menu|message-box|small-caption|status-bar|inherit', + + 'height': r'{length}|{percentage}|auto|inherit', + 'left': r'{length}|{percentage}|auto|inherit', + 'letter-spacing': r'normal|{length}|inherit', + 'line-height': r'{line-height}', + 'list-style-image': r'{list-style-image}', + 'list-style-position': r'{list-style-position}', + 'list-style-type': r'{list-style-type}', + 'list-style': r'{list-attrs}(\s+{list-attrs})*|inherit', + 'margin-right': r'{margin-width}|inherit', + 'margin-left': r'{margin-width}|inherit', + 'margin-top': r'{margin-width}|inherit', + 'margin-bottom': r'{margin-width}|inherit', + 'margin': r'{margin-width}(\s+{margin-width}){0,3}|inherit', + 'max-height': r'{length}|{percentage}|none|inherit', + 'max-width': r'{length}|{percentage}|none|inherit', + 'min-height': r'{length}|{percentage}|none|inherit', + 'min-width': r'{length}|{percentage}|none|inherit', + 'orphans': r'{integer}|inherit', + 'overflow': r'{overflow}', + 'padding-top': r'{padding-width}|inherit', + 'padding-right': r'{padding-width}|inherit', + 'padding-bottom': r'{padding-width}|inherit', + 'padding-left': r'{padding-width}|inherit', + 'padding': r'{padding-width}(\s+{padding-width}){0,3}|inherit', + 'page-break-after': r'auto|always|avoid|left|right|inherit', + 'page-break-before': r'auto|always|avoid|left|right|inherit', + 'page-break-inside': r'avoid|auto|inherit', + 'pause-after': r'{time}|{percentage}|inherit', + 'pause-before': r'{time}|{percentage}|inherit', + 'pause': r'({time}|{percentage}){1,2}|inherit', + 'pitch-range': r'{number}|inherit', + 'pitch': r'{frequency}|x-low|low|medium|high|x-high|inherit', + 'play-during': r'{uri}(\s+(mix|repeat))*|auto|none|inherit', + 'position': r'static|relative|absolute|fixed|inherit', + 'quotes': r'({string}\s+{string})(\s+{string}\s+{string})*|none|inherit', + 'richness': r'{number}|inherit', + 'right': r'{length}|{percentage}|auto|inherit', + 'speak-header': r'once|always|inherit', + 'speak-numeral': r'digits|continuous|inherit', + 'speak-punctuation': r'code|none|inherit', + 'speak': r'normal|none|spell-out|inherit', + 'speech-rate': r'{number}|x-slow|slow|medium|fast|x-fast|faster|slower|inherit', + 'stress': r'{number}|inherit', + 'table-layout': r'auto|fixed|inherit', + 'text-align': r'left|right|center|justify|inherit', + 'text-decoration': r'none|{text-attrs}(\s+{text-attrs})*|inherit', + 'text-indent': r'{length}|{percentage}|inherit', + 'text-transform': r'capitalize|uppercase|lowercase|none|inherit', + 'top': r'{length}|{percentage}|auto|inherit', + 'unicode-bidi': r'normal|embed|bidi-override|inherit', + 'vertical-align': r'baseline|sub|super|top|text-top|middle|bottom|text-bottom|{percentage}|{length}|inherit', + 'visibility': r'visible|hidden|collapse|inherit', + 'voice-family': r'({specific-voice}|{generic-voice}{w},{w})*({specific-voice}|{generic-voice})|inherit', + 'volume': r'{number}|{percentage}|silent|x-soft|soft|medium|loud|x-loud|inherit', + 'white-space': r'normal|pre|nowrap|pre-wrap|pre-line|inherit', + 'widows': r'{integer}|inherit', + 'width': r'{length}|{percentage}|auto|inherit', + 'word-spacing': r'normal|{length}|inherit', + 'z-index': r'auto|{integer}|inherit', +} + + +macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS] = { + 'border-style': 'none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset', + 'border-width': '{length}|thin|medium|thick', + 'b1': r'{border-width}?({w}{border-style})?({w}{color})?', + 'b2': r'{border-width}?({w}{color})?({w}{border-style})?', + 'b3': r'{border-style}?({w}{border-width})?({w}{color})?', + 'b4': r'{border-style}?({w}{color})?({w}{border-width})?', + 'b5': r'{color}?({w}{border-style})?({w}{border-width})?', + 'b6': r'{color}?({w}{border-width})?({w}{border-style})?', + 'border-attrs': r'{b1}|{b2}|{b3}|{b4}|{b5}|{b6}', + 'border-radius-part': '({length}|{percentage})(\s+({length}|{percentage}))?' + } +properties[Profiles.CSS3_BACKGROUNDS_AND_BORDERS] = { + 'border-color': r'({color}|transparent)(\s+({color}|transparent)){0,3}|inherit', + 'border-style': r'{border-style}(\s+{border-style}){0,3}|inherit', + 'border-top': r'{border-attrs}|inherit', + 'border-right': r'{border-attrs}|inherit', + 'border-bottom': r'{border-attrs}|inherit', + 'border-left': r'{border-attrs}|inherit', + 'border-top-color': r'{color}|transparent|inherit', + 'border-right-color': r'{color}|transparent|inherit', + 'border-bottom-color': r'{color}|transparent|inherit', + 'border-left-color': r'{color}|transparent|inherit', + 'border-top-style': r'{border-style}|inherit', + 'border-right-style': r'{border-style}|inherit', + 'border-bottom-style': r'{border-style}|inherit', + 'border-left-style': r'{border-style}|inherit', + 'border-top-width': r'{border-width}|inherit', + 'border-right-width': r'{border-width}|inherit', + 'border-bottom-width': r'{border-width}|inherit', + 'border-left-width': r'{border-width}|inherit', + 'border-width': r'{border-width}(\s+{border-width}){0,3}|inherit', + 'border': r'{border-attrs}|inherit', + 'border-top-right-radius': '{border-radius-part}', + 'border-bottom-right-radius': '{border-radius-part}', + 'border-bottom-left-radius': '{border-radius-part}', + 'border-top-left-radius': '{border-radius-part}', + 'border-radius': '({length}{w}|{percentage}{w}){1,4}(/{w}({length}{w}|{percentage}{w}){1,4})?', + 'box-shadow': 'none|{shadow}({w},{w}{shadow})*', + } + +# CSS3 Basic User Interface Module +macros[Profiles.CSS3_BASIC_USER_INTERFACE] = { + 'border-style': macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS]['border-style'], + 'border-width': macros[Profiles.CSS3_BACKGROUNDS_AND_BORDERS]['border-width'], + 'outline-1': r'{outline-color}(\s+{outline-style})?(\s+{outline-width})?', + 'outline-2': r'{outline-color}(\s+{outline-width})?(\s+{outline-style})?', + 'outline-3': r'{outline-style}(\s+{outline-color})?(\s+{outline-width})?', + 'outline-4': r'{outline-style}(\s+{outline-width})?(\s+{outline-color})?', + 'outline-5': r'{outline-width}(\s+{outline-color})?(\s+{outline-style})?', + 'outline-6': r'{outline-width}(\s+{outline-style})?(\s+{outline-color})?', + 'outline-color': r'{color}|invert|inherit', + 'outline-style': r'auto|{border-style}|inherit', + 'outline-width': r'{border-width}|inherit', + } +properties[Profiles.CSS3_BASIC_USER_INTERFACE] = { + 'box-sizing': r'content-box|border-box', + 'cursor': r'((({uri}{w}({number}{w}{number}{w})?,{w})*)?(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|text|vertical-text|alias|copy|move|no-drop|not-allowed|(e|n|ne|nw|s|se|sw|w|ew|ns|nesw|nwse|col|row)-resize|all-scroll))|inherit', + 'nav-index': r'auto|{number}|inherit', + 'outline-color': r'{outline-color}', + 'outline-style': r'{outline-style}', + 'outline-width': r'{outline-width}', + 'outline-offset': r'{length}|inherit', + #'outline': r'{outline-attrs}(\s+{outline-attrs})*|inherit', + 'outline': r'{outline-1}|{outline-2}|{outline-3}|{outline-4}|{outline-5}|{outline-6}|inherit', + 'resize': 'none|both|horizontal|vertical|inherit', + } + +# CSS Box Module Level 3 +macros[Profiles.CSS3_BOX] = { + 'overflow': macros[Profiles.CSS_LEVEL_2]['overflow'] + } +properties[Profiles.CSS3_BOX] = { + 'overflow': '{overflow}{w}{overflow}?|inherit', + 'overflow-x': '{overflow}|inherit', + 'overflow-y': '{overflow}|inherit' + } + +# CSS Color Module Level 3 +macros[Profiles.CSS3_COLOR] = { + # orange and transparent in CSS 2.1 + 'namedcolor': r'(currentcolor|transparent|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)', + # orange? + 'rgbacolor': r'rgba\({w}{int}{w}\,{w}{int}{w}\,{w}{int}{w}\,{w}{num}{w}\)|rgba\({w}{num}%{w}\,{w}{num}%{w}\,{w}{num}%{w}\,{w}{num}{w}\)', + 'hslcolor': r'hsl\({w}{int}{w}\,{w}{num}%{w}\,{w}{num}%{w}\)|hsla\({w}{int}{w}\,{w}{num}%{w}\,{w}{num}%{w}\,{w}{num}{w}\)', + 'x11color': r'aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen', + 'uicolor': r'(ActiveBorder|ActiveCaption|AppWorkspace|Background|ButtonFace|ButtonHighlight|ButtonShadow|ButtonText|CaptionText|GrayText|Highlight|HighlightText|InactiveBorder|InactiveCaption|InactiveCaptionText|InfoBackground|InfoText|Menu|MenuText|Scrollbar|ThreeDDarkShadow|ThreeDFace|ThreeDHighlight|ThreeDLightShadow|ThreeDShadow|Window|WindowFrame|WindowText)', + 'color': r'{namedcolor}|{hexcolor}|{rgbcolor}|{rgbacolor}|{hslcolor}|{x11color}|inherit', + } +properties[Profiles.CSS3_COLOR] = { + 'opacity': r'{num}|inherit', + } + +# CSS Fonts Module Level 3 http://www.w3.org/TR/css3-fonts/ +macros[Profiles.CSS3_FONTS] = { + #'family-name': r'{string}|{ident}', + 'family-name': r'{string}|{ident}({w}{ident})*', + 'font-face-name': 'local\({w}{family-name}{w}\)', + 'font-stretch-names': r'(ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded)', + 'unicode-range': r'[uU]\+[0-9A-Fa-f?]{1,6}(\-[0-9A-Fa-f]{1,6})?' + } +properties[Profiles.CSS3_FONTS] = { + 'font-size-adjust': r'{number}|none|inherit', + 'font-stretch': r'normal|wider|narrower|{font-stretch-names}|inherit' + } +properties[Profiles.CSS3_FONT_FACE] = { + 'font-family': '{family-name}', + 'font-stretch': r'{font-stretch-names}', + 'font-style': r'normal|italic|oblique', + 'font-weight': r'normal|bold|[1-9]00', + 'src': r'({uri}{w}(format\({w}{string}{w}(\,{w}{string}{w})*\))?|{font-face-name})({w},{w}({uri}{w}(format\({w}{string}{w}(\,{w}{string}{w})*\))?|{font-face-name}))*', + 'unicode-range': '{unicode-range}({w},{w}{unicode-range})*' + } + +# CSS3 Paged Media +macros[Profiles.CSS3_PAGED_MEDIA] = { + 'page-size': 'a5|a4|a3|b5|b4|letter|legal|ledger', + 'page-orientation': 'portrait|landscape', + 'page-1': '{page-size}(?:{w}{page-orientation})?', + 'page-2': '{page-orientation}(?:{w}{page-size})?', + 'page-size-orientation': '{page-1}|{page-2}', + 'pagebreak': 'auto|always|avoid|left|right' + } +properties[Profiles.CSS3_PAGED_MEDIA] = { + 'fit': 'fill|hidden|meet|slice', + 'fit-position': r'auto|(({percentage}|{length})(\s*({percentage}|{length}))?|((top|center|bottom)\s*(left|center|right)?)|((left|center|right)\s*(top|center|bottom)?))', + 'image-orientation': 'auto|{angle}', + 'orphans': r'{integer}|inherit', + 'page': 'auto|{ident}', + 'page-break-before': '{pagebreak}|inherit', + 'page-break-after': '{pagebreak}|inherit', + 'page-break-inside': 'auto|avoid|inherit', + 'size': '({length}{w}){1,2}|auto|{page-size-orientation}', + 'widows': r'{integer}|inherit' + } + +macros[Profiles.CSS3_TEXT] = { + } +properties[Profiles.CSS3_TEXT] = { + 'text-shadow': 'none|{shadow}({w},{w}{shadow})*', + } diff --git a/libs/cssutils/sac.py b/libs/cssutils/sac.py new file mode 100755 index 00000000..d46a0b70 --- /dev/null +++ b/libs/cssutils/sac.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python +"""A validating CSSParser""" +__all__ = ['CSSParser'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: parse.py 1754 2009-05-30 14:50:13Z cthedot $' + +import helper +import codecs +import errorhandler +import os +import tokenize2 +import urllib +import sys + + +class ErrorHandler(object): + """Basic class for CSS error handlers. + + This class class provides a default implementation ignoring warnings and + recoverable errors and throwing a SAXParseException for fatal errors. + + If a CSS application needs to implement customized error handling, it must + extend this class and then register an instance with the CSS parser + using the parser's setErrorHandler method. The parser will then report all + errors and warnings through this interface. + + The parser shall use this class instead of throwing an exception: it is + up to the application whether to throw an exception for different types of + errors and warnings. Note, however, that there is no requirement that the + parser continue to provide useful information after a call to fatalError + (in other words, a CSS driver class could catch an exception and report a + fatalError). + """ + def __init__(self): + self._log = errorhandler.ErrorHandler() + + def error(self, exception, token=None): + self._log.error(exception, token, neverraise=True) + + def fatal(self, exception, token=None): + self._log.fatal(exception, token) + + def warn(self, exception, token=None): + self._log.warn(exception, token, neverraise=True) + + +class DocumentHandler(object): + """ + void endFontFace() + Receive notification of the end of a font face statement. + void endMedia(SACMediaList media) + Receive notification of the end of a media statement. + void endPage(java.lang.String name, java.lang.String pseudo_page) + Receive notification of the end of a media statement. + void importStyle(java.lang.String uri, SACMediaList media, java.lang.String defaultNamespaceURI) + Receive notification of a import statement in the style sheet. + void startFontFace() + Receive notification of the beginning of a font face statement. + void startMedia(SACMediaList media) + Receive notification of the beginning of a media statement. + void startPage(java.lang.String name, java.lang.String pseudo_page) + Receive notification of the beginning of a page statement. + """ + def __init__(self): + def log(msg): + sys.stderr.write('INFO\t%s\n' % msg) + self._log = log + + def comment(self, text, line=None, col=None): + "Receive notification of a comment." + self._log("comment %r at [%s, %s]" % (text, line, col)) + + def startDocument(self, encoding): + "Receive notification of the beginning of a style sheet." + # source + self._log("startDocument encoding=%s" % encoding) + + def endDocument(self, source=None, line=None, col=None): + "Receive notification of the end of a document." + self._log("endDocument EOF") + + def importStyle(self, uri, media, name, line=None, col=None): + "Receive notification of a import statement in the style sheet." + # defaultNamespaceURI??? + self._log("importStyle at [%s, %s]" % (line, col)) + + def namespaceDeclaration(self, prefix, uri, line=None, col=None): + "Receive notification of an unknown rule t-rule not supported by this parser." + # prefix might be None! + self._log("namespaceDeclaration at [%s, %s]" % (line, col)) + + def startSelector(self, selectors=None, line=None, col=None): + "Receive notification of the beginning of a rule statement." + # TODO selectorList! + self._log("startSelector at [%s, %s]" % (line, col)) + + def endSelector(self, selectors=None, line=None, col=None): + "Receive notification of the end of a rule statement." + self._log("endSelector at [%s, %s]" % (line, col)) + + def property(self, name, value='TODO', important=False, line=None, col=None): + "Receive notification of a declaration." + # TODO: value is LexicalValue? + self._log("property %r at [%s, %s]" % (name, line, col)) + + def ignorableAtRule(self, atRule, line=None, col=None): + "Receive notification of an unknown rule t-rule not supported by this parser." + self._log("ignorableAtRule %r at [%s, %s]" % (atRule, line, col)) + + + +class EchoHandler(DocumentHandler): + "Echos all input to property `out`" + def __init__(self): + super(EchoHandler, self).__init__() + self._out = [] + + out = property(lambda self: u''.join(self._out)) + + def startDocument(self, encoding): + super(EchoHandler, self).startDocument(encoding) + if u'utf-8' != encoding: + self._out.append(u'@charset "%s";\n' % encoding) + +# def comment(self, text, line=None, col=None): +# self._out.append(u'/*%s*/' % text) + + def importStyle(self, uri, media, name, line=None, col=None): + "Receive notification of a import statement in the style sheet." + # defaultNamespaceURI??? + super(EchoHandler, self).importStyle(uri, media, name, line, col) + self._out.append(u'@import %s%s%s;\n' % (helper.string(uri), + u'%s ' % media if media else u'', + u'%s ' % name if name else u'') + ) + + + def namespaceDeclaration(self, prefix, uri, line=None, col=None): + super(EchoHandler, self).namespaceDeclaration(prefix, uri, line, col) + self._out.append(u'@namespace %s%s;\n' % (u'%s ' % prefix if prefix else u'', + helper.string(uri))) + + def startSelector(self, selectors=None, line=None, col=None): + super(EchoHandler, self).startSelector(selectors, line, col) + if selectors: + self._out.append(u', '.join(selectors)) + self._out.append(u' {\n') + + def endSelector(self, selectors=None, line=None, col=None): + self._out.append(u' }') + + def property(self, name, value, important=False, line=None, col=None): + super(EchoHandler, self).property(name, value, line, col) + self._out.append(u' %s: %s%s;\n' % (name, value, + u' !important' if important else u'')) + + +class Parser(object): + """ + java.lang.String getParserVersion() + Returns a string about which CSS language is supported by this parser. + boolean parsePriority(InputSource source) + Parse a CSS priority value (e.g. + LexicalUnit parsePropertyValue(InputSource source) + Parse a CSS property value. + void parseRule(InputSource source) + Parse a CSS rule. + SelectorList parseSelectors(InputSource source) + Parse a comma separated list of selectors. + void parseStyleDeclaration(InputSource source) + Parse a CSS style declaration (without '{' and '}'). + void parseStyleSheet(InputSource source) + Parse a CSS document. + void parseStyleSheet(java.lang.String uri) + Parse a CSS document from a URI. + void setConditionFactory(ConditionFactory conditionFactory) + + void setDocumentHandler(DocumentHandler handler) + Allow an application to register a document event handler. + void setErrorHandler(ErrorHandler handler) + Allow an application to register an error event handler. + void setLocale(java.util.Locale locale) + Allow an application to request a locale for errors and warnings. + void setSelectorFactory(SelectorFactory selectorFactory) + """ + def __init__(self, documentHandler=None, errorHandler=None): + self._tokenizer = tokenize2.Tokenizer() + if documentHandler: + self.setDocumentHandler(documentHandler) + else: + self.setDocumentHandler(DocumentHandler()) + + if errorHandler: + self.setErrorHandler(errorHandler) + else: + self.setErrorHandler(ErrorHandler()) + + def parseString(self, cssText, encoding=None): + if isinstance(cssText, str): + cssText = codecs.getdecoder('css')(cssText, encoding=encoding)[0] + + tokens = self._tokenizer.tokenize(cssText, fullsheet=True) + + def COMMENT(val, line, col): + self._handler.comment(val[2:-2], line, col) + + def EOF(val, line, col): + self._handler.endDocument(val, line, col) + + def simple(t): + map = {'COMMENT': COMMENT, + 'S': lambda val, line, col: None, + 'EOF': EOF} + type_, val, line, col = t + if type_ in map: + map[type_](val, line, col) + return True + else: + return False + + # START PARSING + t = tokens.next() + type_, val, line, col = t + + encoding = 'utf-8' + if 'CHARSET_SYM' == type_: + # @charset "encoding"; + # S + encodingtoken = tokens.next() + semicolontoken = tokens.next() + if 'STRING' == type_: + encoding = helper.stringvalue(val) + # ; + if 'STRING' == encodingtoken[0] and semicolontoken: + encoding = helper.stringvalue(encodingtoken[1]) + else: + self._errorHandler.fatal(u'Invalid @charset') + + t = tokens.next() + type_, val, line, col = t + + self._handler.startDocument(encoding) + + while True: + start = (line, col) + try: + if simple(t): + pass + + elif 'ATKEYWORD' == type_ or type_ in ('PAGE_SYM', 'MEDIA_SYM', 'FONT_FACE_SYM'): + atRule = [val] + braces = 0 + while True: + # read till end ; + # TODO: or {} + t = tokens.next() + type_, val, line, col = t + atRule.append(val) + if u';' == val and not braces: + break + elif u'{' == val: + braces += 1 + elif u'}' == val: + braces -= 1 + if braces == 0: + break + + self._handler.ignorableAtRule(u''.join(atRule), *start) + + elif 'IMPORT_SYM' == type_: + # import URI or STRING media? name? + uri, media, name = None, None, None + while True: + t = tokens.next() + type_, val, line, col = t + if 'STRING' == type_: + uri = helper.stringvalue(val) + elif 'URI' == type_: + uri = helper.urivalue(val) + elif u';' == val: + break + + if uri: + self._handler.importStyle(uri, media, name) + else: + self._errorHandler.error(u'Invalid @import' + u' declaration at %r' + % (start,)) + + elif 'NAMESPACE_SYM' == type_: + prefix, uri = None, None + while True: + t = tokens.next() + type_, val, line, col = t + if 'IDENT' == type_: + prefix = val + elif 'STRING' == type_: + uri = helper.stringvalue(val) + elif 'URI' == type_: + uri = helper.urivalue(val) + elif u';' == val: + break + if uri: + self._handler.namespaceDeclaration(prefix, uri, *start) + else: + self._errorHandler.error(u'Invalid @namespace' + u' declaration at %r' + % (start,)) + + else: + # CSSSTYLERULE + selector = [] + selectors = [] + while True: + # selectors[, selector]* { + if 'S' == type_: + selector.append(u' ') + elif simple(t): + pass + elif u',' == val: + selectors.append(u''.join(selector).strip()) + selector = [] + elif u'{' == val: + selectors.append(u''.join(selector).strip()) + self._handler.startSelector(selectors, *start) + break + else: + selector.append(val) + + t = tokens.next() + type_, val, line, col = t + + end = None + while True: + # name: value [!important][;name: value [!important]]*;? + name, value, important = None, [], False + + while True: + # name: + t = tokens.next() + type_, val, line, col = t + if 'S' == type_: + pass + elif simple(t): + pass + elif 'IDENT' == type_: + if name: + self._errorHandler.error('more than one property name', t) + else: + name = val + elif u':' == val: + if not name: + self._errorHandler.error('no property name', t) + break + elif u';' == val: + self._errorHandler.error('premature end of property', t) + end = val + break + elif u'}' == val: + if name: + self._errorHandler.error('premature end of property', t) + end = val + break + else: + self._errorHandler.error('unexpected property name token %r' % val, t) + + while not u';' == end and not u'}' == end: + # value !;} + t = tokens.next() + type_, val, line, col = t + + if 'S' == type_: + value.append(u' ') + elif simple(t): + pass + elif u'!' == val or u';' == val or u'}' == val: + value = ''.join(value).strip() + if not value: + self._errorHandler.error('premature end of property (no value)', t) + end = val + break + else: + value.append(val) + + while u'!' == end: + # !important + t = tokens.next() + type_, val, line, col = t + + if simple(t): + pass + elif u'IDENT' == type_ and not important: + important = True + elif u';' == val or u'}' == val: + end = val + break + else: + self._errorHandler.error('unexpected priority token %r' % val) + + if name and value: + self._handler.property(name, value, important) + + if u'}' == end: + self._handler.endSelector(selectors, line=line, col=col) + break + else: + # reset + end = None + + else: + self._handler.endSelector(selectors, line=line, col=col) + + t = tokens.next() + type_, val, line, col = t + + except StopIteration: + break + + + + def setDocumentHandler(self, handler): + "Allow an application to register a document event `handler`." + self._handler = handler + + def setErrorHandler(self, handler): + "TODO" + self._errorHandler = handler + \ No newline at end of file diff --git a/libs/cssutils/script.py b/libs/cssutils/script.py new file mode 100755 index 00000000..56c1c6e2 --- /dev/null +++ b/libs/cssutils/script.py @@ -0,0 +1,362 @@ +"""classes and functions used by cssutils scripts +""" +__all__ = ['CSSCapture', 'csscombine'] +__docformat__ = 'restructuredtext' +__version__ = '$Id: parse.py 1323 2008-07-06 18:13:57Z cthedot $' + +import HTMLParser +import codecs +import cssutils +import errno +import logging +import os +import sys +import urllib2 +import urlparse + +try: + import cssutils.encutils as encutils +except ImportError: + try: + import encutils + except ImportError: + sys.exit("You need encutils from http://cthedot.de/encutils/") + +# types of sheets in HTML +LINK = 0 # +STYLE = 1 # + +class CSSCaptureHTMLParser(HTMLParser.HTMLParser): + """CSSCapture helper: Parse given data for link and style elements""" + curtag = u'' + sheets = [] # (type, [atts, cssText]) + + def _loweratts(self, atts): + return dict([(a.lower(), v.lower()) for a, v in atts]) + + def handle_starttag(self, tag, atts): + if tag == u'link': + atts = self._loweratts(atts) + if u'text/css' == atts.get(u'type', u''): + self.sheets.append((LINK, atts)) + elif tag == u'style': + # also get content of style + atts = self._loweratts(atts) + if u'text/css' == atts.get(u'type', u''): + self.sheets.append((STYLE, [atts, u''])) + self.curtag = tag + else: + # close as only intersting