Merge branch 'release/0.10.0'

This commit is contained in:
Adam 2015-08-06 19:06:26 +08:00
commit 1e6c161887
2673 changed files with 13549 additions and 60000 deletions

10
.gitignore vendored
View file

@ -1,5 +1,5 @@
# SB User Related #
######################
# SB User Related #
cache/*
cache.db*
config.ini*
@ -11,18 +11,18 @@ server.crt
server.key
restore/
# SB Test Related #
######################
# SB Test Related #
tests/Logs/*
tests/sickbeard.*
tests/cache.db
# Compiled source #
######################
# Compiled source #
*.py[co]
# IDE specific #
######################
# IDE specific #
*.bak
*.tmp
*.wpr
@ -35,8 +35,8 @@ tests/cache.db
Session.vim
.ropeproject/*
# OS generated files #
######################
# OS generated files #
.Spotlight-V100
.Trashes
.DS_Store

View file

@ -6,6 +6,8 @@ python:
install:
- pip install cheetah
- pip install coveralls
before_script: cd ./tests
script: python all_tests.py
script: coverage run --source=.. --omit=../lib/*,../tornado/* all_tests.py
after_success: coveralls

View file

@ -1,3 +1,115 @@
### 0.10.0 (2015-08-06 11:05:00 UTC)
* Remove EZRSS provider
* Update Tornado webserver to 4.2 (fdfaf3d)
* Update change to suppress reporting of Tornado exception error 1 to updated package (ref:hacks.txt)
* Update fix for API response header for JSON content type and the return of JSONP data to updated package (ref:hacks.txt)
* Update Requests library 2.6.2 to 2.7.0 (8b5e457)
* Update change to suppress HTTPS verification InsecureRequestWarning to updated package (ref:hacks.txt)
* Change to consolidate cache database migration code
* Change to only rebuild namecache on show update instead of on every search
* Change to allow file moving across partition
* Add removal of old entries from namecache on show deletion
* Add Hallmark and specific ITV logos, remove logo of non-english Comedy Central Family
* Fix provider TD failing to find episodes of air by date shows
* Fix provider SCC failing to find episodes of air by date shows
* Fix provider SCC searching propers
* Fix provider SCC stop snatching releases for episodes already completed
* Fix provider SCC handle null server responses
* Change provider SCC remove 1 of 3 requests per search to save 30% time
* Change provider SCC login process to use General Config/Advanced/Proxy host setting
* Change provider SCD PEP8 and code convention cleanup
* Change provider HDB code simplify and PEP8
* Change provider IPT only decode unicode search strings
* Change provider IPT login process to use General Config/Advanced/Proxy host setting
* Change provider TB logo icon used on Config/Search Providers
* Change provider TB PEP8 and code convention cleanup
* Change provider TB login process to use General Config/Advanced/Proxy host setting
* Remove useless webproxies from provider TPB as they fail for one reason or another
* Change provider TPB to use mediaExtensions from common instead of hard-coded private list
* Add new tld variants to provider TPB
* Add test for authenticity to provider TPB to notify of 3rd party block
* Change provider TD logo icon used on Config/Search Providers
* Change provider TD login process to use General Config/Advanced/Proxy host setting
* Change provider BTN code simplify and PEP8
* Change provider BTS login process to use General Config/Advanced/Proxy host setting
* Change provider FSH login process to use General Config/Advanced/Proxy host setting
* Change provider RSS torrent code to use General Config/Advanced/Proxy host setting, simplify and PEP8
* Change provider Wombles's PEP8 and code convention cleanup
* Change provider Womble's use SSL
* Change provider KAT remove dead url
* Change provider KAT to use mediaExtensions from common instead of private list
* Change provider KAT provider PEP8 and code convention cleanup
* Change refactor and code simplification for torrent and newznab providers
* Change refactor SCC to use torrent provider simplification and PEP8
* Change refactor SCD to use torrent provider simplification
* Change refactor TB to use torrent provider simplification and PEP8
* Change refactor TBP to use torrent provider simplification and PEP8
* Change refactor TD to use torrent provider simplification and PEP8
* Change refactor TL to use torrent provider simplification and PEP8
* Change refactor BTS to use torrent provider simplification and PEP8
* Change refactor FSH to use torrent provider simplification and PEP8
* Change refactor IPT to use torrent provider simplification and PEP8
* Change refactor KAT to use torrent provider simplification and PEP8
* Change refactor TOTV to use torrent provider simplification and PEP8
* Remove HDTorrents torrent provider
* Remove NextGen torrent provider
* Add Rarbg torrent provider
* Add MoreThan torrent provider
* Add AlphaRatio torrent provider
* Add PiSexy torrent provider
* Add Strike torrent provider
* Add TorrentShack torrent provider
* Add BeyondHD torrent provider
* Add GFTracker torrent provider
* Add TtN torrent provider
* Add GTI torrent provider
* Fix getManualSearchStatus: object has no attribute 'segment'
* Change handling of general HTTP error response codes to prevent issues
* Add handling for CloudFlare custom HTTP response codes
* Fix to correctly load local libraries instead of system installed libraries
* Update PyNMA to hybrid v1.0
* Change first run after install to set up the main db to the current schema instead of upgrading
* Change don't create a backup from an initial zero byte main database file, PEP8 and code tidy up
* Fix show list view when no shows exist and "Group show lists shows into" is set to anything other than "One Show List"
* Fix fault matching air by date shows by using correct episode/season strings in find search results
* Change add 'hevc', 'x265' and some langs to Config Search/Episode Search/Ignore result with any word
* Change NotifyMyAndroid to its new web location
* Update feedparser library 5.1.3 to 5.2.0 (8c62940)
* Remove feedcache implementation and library
* Add coverage testing and coveralls support
* Add py2/3 regression testing for exception clauses
* Change py2 exception clauses to py2/3 compatible clauses
* Change py2 print statements to py2/3 compatible functions
* Change py2 octal literals into the new py2/3 syntax
* Change py2 iteritems to py2/3 compatible statements using six library
* Change py2 queue, httplib, cookielib and xmlrpclib to py2/3 compatible calls using six
* Change py2 file and reload functions to py2/3 compatible open and reload_module functions
* Change Kodi notifier to use requests as opposed to urllib
* Change to consolidate scene exceptions and name cache code
* Change check_url function to use requests instead of httplib library
* Update Six compatibility library 1.5.2 to 1.9.0 (8a545f4)
* Update SimpleJSON library 2.0.9 to 3.7.3 (0bcdf20)
* Update xmltodict library 0.9.0 to 0.9.2 (579a005)
* Update dateutil library 2.2 to 2.4.2 (a6b8925)
* Update ConfigObj library 4.6.0 to 5.1.0 (a68530a)
* Update Beautiful Soup to 4.3.2 (r353)
* Update jsonrpclib library r20 to (b59217c)
* Change cachecontrol library to ensure cache file exists before attempting delete
* Fix saving root dirs
* Change pushbullet from urllib2 to requests
* Change to make pushbullet error messages clearer
* Change pyNMA use of urllib to requests (ref:hacks.txt)
* Change Trakt url to fix baseline uses (e.g. add from trending)
* Fix edit on show page for shows that have anime enabled in mass edit
* Fix issue parsing items in ToktoToshokan provider
* Change to only show option "End upgrade on first match" on edit show page if quality custom is selected
* Change label "Show is grouped in" in edit show page to "Show is in group" and move the section higher
* Fix post processing of anime with version tags
* Change accept SD titles that contain audio quality
* Change readme.md
### 0.9.1 (2015-05-25 03:03:00 UTC)
* Fix erroneous multiple downloads of torrent files which causes snatches to fail under certain conditions
@ -64,7 +176,7 @@
* Change disable the Force buttons on the Manage Searches page while a search is running
* Change staggered periods of testing and updating of all shows "ended" status up to 460 days
* Change "Archive" to "Upgrade to" in Edit show and other places and improve related texts for clarity
* Fix history consolidation to only update an episode status if the history disagrees with the status.
* Fix history consolidation to only update an episode status if the history disagrees with the status
### 0.8.3 (2015-04-25 08:48:00 UTC)

View file

@ -3,3 +3,5 @@ Libs with customisations...
/tornado
/lib/requests/packages/urllib3/connectionpool.py
/lib/requests/packages/urllib3/util/ssl_.py
/lib/cachecontrol/caches/file_cache.py
/lib/pynma/pynma.py

View file

@ -18,6 +18,7 @@
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
# Check needed software dependencies to nudge users to fix their setup
from __future__ import print_function
from __future__ import with_statement
import time
@ -32,7 +33,7 @@ import threading
import getopt
if sys.version_info < (2, 6):
print 'Sorry, requires Python 2.6 or 2.7.'
print('Sorry, requires Python 2.6 or 2.7.')
sys.exit(1)
try:
@ -41,13 +42,14 @@ try:
if Cheetah.Version[0] != '2':
raise ValueError
except ValueError:
print 'Sorry, requires Python module Cheetah 2.1.0 or newer.'
print('Sorry, requires Python module Cheetah 2.1.0 or newer.')
sys.exit(1)
except:
print 'The Python module Cheetah is required'
print('The Python module Cheetah is required')
sys.exit(1)
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
from lib.six import moves
# We only need this for compiling an EXE and I will just always do that on 2.6+
if sys.hexversion >= 0x020600F0:
@ -139,15 +141,15 @@ class SickGear(object):
sickbeard.SYS_ENCODING = 'UTF-8'
if not hasattr(sys, 'setdefaultencoding'):
reload(sys)
moves.reload_module(sys)
try:
# pylint: disable=E1101
# On non-unicode builds this will raise an AttributeError, if encoding type is not valid it throws a LookupError
sys.setdefaultencoding(sickbeard.SYS_ENCODING)
except:
print 'Sorry, you MUST add the SickGear folder to the PYTHONPATH environment variable'
print 'or find another way to force Python to use %s for string encoding.' % sickbeard.SYS_ENCODING
print('Sorry, you MUST add the SickGear folder to the PYTHONPATH environment variable')
print('or find another way to force Python to use %s for string encoding.' % sickbeard.SYS_ENCODING)
sys.exit(1)
# Need console logging for SickBeard.py and SickBeard-console.exe
@ -231,7 +233,7 @@ class SickGear(object):
else:
if self.consoleLogging:
print u'Not running in daemon mode. PID file creation disabled'
print(u'Not running in daemon mode. PID file creation disabled')
self.CREATEPID = False
@ -242,7 +244,7 @@ class SickGear(object):
# Make sure that we can create the data dir
if not os.access(sickbeard.DATA_DIR, os.F_OK):
try:
os.makedirs(sickbeard.DATA_DIR, 0744)
os.makedirs(sickbeard.DATA_DIR, 0o744)
except os.error:
sys.exit(u'Unable to create data directory: %s Exiting.' % sickbeard.DATA_DIR)
@ -260,11 +262,11 @@ class SickGear(object):
os.chdir(sickbeard.DATA_DIR)
if self.consoleLogging:
print u'Starting up SickGear from %s' % sickbeard.CONFIG_FILE
print(u'Starting up SickGear from %s' % sickbeard.CONFIG_FILE)
# Load the config and publish it to the sickbeard package
if not os.path.isfile(sickbeard.CONFIG_FILE):
print u'Unable to find "%s", all settings will be default!' % sickbeard.CONFIG_FILE
print(u'Unable to find "%s", all settings will be default!' % sickbeard.CONFIG_FILE)
sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE)
@ -272,12 +274,12 @@ class SickGear(object):
if CUR_DB_VERSION > 0:
if CUR_DB_VERSION < MIN_DB_VERSION:
print u'Your database version (%s) is too old to migrate from with this version of SickGear' \
% CUR_DB_VERSION
print(u'Your database version (%s) is too old to migrate from with this version of SickGear' \
% CUR_DB_VERSION)
sys.exit(u'Upgrade using a previous version of SG first, or start with no database file to begin fresh')
if CUR_DB_VERSION > MAX_DB_VERSION:
print u'Your database version (%s) has been incremented past what this version of SickGear supports' \
% CUR_DB_VERSION
print(u'Your database version (%s) has been incremented past what this version of SickGear supports' \
% CUR_DB_VERSION)
sys.exit(
u'If you have used other forks of SG, your database may be unusable due to their modifications')
@ -361,6 +363,10 @@ class SickGear(object):
# refresh network timezones
network_timezones.update_network_dict()
# load all ids from xem
startup_background_tasks = threading.Thread(name='FETCH-XEMDATA', target=sickbeard.scene_exceptions.get_xem_ids)
startup_background_tasks.start()
# sure, why not?
if sickbeard.USE_FAILED_DOWNLOADS:
failed_history.trimHistory()
@ -387,7 +393,7 @@ class SickGear(object):
pid = os.fork() # @UndefinedVariable - only available in UNIX
if pid != 0:
os._exit(0)
except OSError, e:
except OSError as e:
sys.stderr.write('fork #1 failed: %d (%s)\n' % (e.errno, e.strerror))
sys.exit(1)
@ -402,7 +408,7 @@ class SickGear(object):
pid = os.fork() # @UndefinedVariable - only available in UNIX
if pid != 0:
os._exit(0)
except OSError, e:
except OSError as e:
sys.stderr.write('fork #2 failed: %d (%s)\n' % (e.errno, e.strerror))
sys.exit(1)
@ -411,8 +417,8 @@ class SickGear(object):
pid = str(os.getpid())
logger.log(u'Writing PID: %s to %s' % (pid, self.PIDFILE))
try:
file(self.PIDFILE, 'w').write('%s\n' % pid)
except IOError, e:
open(self.PIDFILE, 'w').write('%s\n' % pid)
except IOError as e:
logger.log_error_and_exit(
u'Unable to write PID file: %s Error: %s [%s]' % (self.PIDFILE, e.strerror, e.errno))
@ -421,9 +427,9 @@ class SickGear(object):
sys.stderr.flush()
devnull = getattr(os, 'devnull', '/dev/null')
stdin = file(devnull, 'r')
stdout = file(devnull, 'a+')
stderr = file(devnull, 'a+')
stdin = open(devnull, 'r')
stdout = open(devnull, 'a+')
stderr = open(devnull, 'a+')
os.dup2(stdin.fileno(), sys.stdin.fileno())
os.dup2(stdout.fileno(), sys.stdout.fileno())
os.dup2(stderr.fileno(), sys.stderr.fileno())
@ -456,7 +462,7 @@ class SickGear(object):
curShow = TVShow(int(sqlShow['indexer']), int(sqlShow['indexer_id']))
curShow.nextEpisode()
sickbeard.showList.append(curShow)
except Exception, e:
except Exception as e:
logger.log(
u'There was an error creating the show in %s: %s' % (sqlShow['location'], str(e).decode('utf-8',
'replace')),

View file

@ -18,14 +18,15 @@
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
from __future__ import with_statement
import os.path
import sys
sickbeardPath = os.path.split(os.path.split(sys.argv[0])[0])[0]
sys.path.append(os.path.join(sickbeardPath, 'lib'))
sys.path.append(sickbeardPath)
sys.path.insert(1, os.path.join(sickbeardPath, 'lib'))
sys.path.insert(1, sickbeardPath)
try:
import requests
@ -132,7 +133,7 @@ def processEpisode(dir_to_process, org_NZB_name=None, status=None):
sess.post(login_url, data={'username': username, 'password': password}, stream=True, verify=False)
result = sess.get(url, params=params, stream=True, verify=False)
if result.status_code == 401:
print 'Verify and use correct username and password in autoProcessTV.cfg'
print('Verify and use correct username and password in autoProcessTV.cfg')
else:
for line in result.iter_lines():
if line:

View file

@ -19,12 +19,13 @@
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import sys
import autoProcessTV
if len(sys.argv) < 4:
print 'No folder supplied - is this being called from HellaVCR?'
print('No folder supplied - is this being called from HellaVCR?')
sys.exit()
else:
autoProcessTV.processEpisode(sys.argv[3], sys.argv[2])

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python2
from __future__ import print_function
import sys
import os
import time
@ -6,8 +7,8 @@ import ConfigParser
import logging
sickbeardPath = os.path.split(os.path.split(sys.argv[0])[0])[0]
sys.path.append(os.path.join(sickbeardPath, 'lib'))
sys.path.append(sickbeardPath)
sys.path.insert(1, os.path.join(sickbeardPath, 'lib'))
sys.path.insert(1, sickbeardPath)
configFilename = os.path.join(sickbeardPath, 'config.ini')
try:
@ -22,9 +23,11 @@ try:
fp = open(configFilename, 'r')
config.readfp(fp)
fp.close()
except IOError, e:
print 'Could not find/read Sickbeard config.ini: ' + str(e)
print 'Possibly wrong mediaToSickbeard.py location. Ensure the file is in the autoProcessTV subdir of your Sickbeard installation'
except IOError as e:
print('Could not find/read Sickbeard config.ini: ' + str(e))
print(
'Possibly wrong mediaToSickbeard.py location. Ensure the file is in the autoProcessTV subdir of your Sickbeard '
'installation')
time.sleep(3)
sys.exit(1)
@ -41,7 +44,7 @@ logfile = os.path.join(logdir, 'sickbeard.log')
try:
handler = logging.FileHandler(logfile)
except:
print 'Unable to open/create the log file at ' + logfile
print('Unable to open/create the log file at ' + logfile)
time.sleep(3)
sys.exit()
@ -53,7 +56,7 @@ def utorrent():
# print 'Calling utorrent'
if len(sys.argv) < 2:
scriptlogger.error('No folder supplied - is this being called from uTorrent?')
print 'No folder supplied - is this being called from uTorrent?'
print('No folder supplied - is this being called from uTorrent?')
time.sleep(3)
sys.exit()
@ -73,7 +76,7 @@ def deluge():
if len(sys.argv) < 4:
scriptlogger.error('No folder supplied - is this being called from Deluge?')
print 'No folder supplied - is this being called from Deluge?'
print('No folder supplied - is this being called from Deluge?')
time.sleep(3)
sys.exit()
@ -86,7 +89,7 @@ def blackhole():
if None != os.getenv('TR_TORRENT_DIR'):
scriptlogger.debug('Processing script triggered by Transmission')
print 'Processing script triggered by Transmission'
print('Processing script triggered by Transmission')
scriptlogger.debug(u'TR_TORRENT_DIR: ' + os.getenv('TR_TORRENT_DIR'))
scriptlogger.debug(u'TR_TORRENT_NAME: ' + os.getenv('TR_TORRENT_NAME'))
dirName = os.getenv('TR_TORRENT_DIR')
@ -94,7 +97,7 @@ def blackhole():
else:
if len(sys.argv) < 2:
scriptlogger.error('No folder supplied - Your client should invoke the script with a Dir and a Relese Name')
print 'No folder supplied - Your client should invoke the script with a Dir and a Release Name'
print('No folder supplied - Your client should invoke the script with a Dir and a Release Name')
time.sleep(3)
sys.exit()
@ -127,13 +130,13 @@ def main():
if not use_torrents:
scriptlogger.error(u'Enable Use Torrent on Sickbeard to use this Script. Aborting!')
print u'Enable Use Torrent on Sickbeard to use this Script. Aborting!'
print(u'Enable Use Torrent on Sickbeard to use this Script. Aborting!')
time.sleep(3)
sys.exit()
if not torrent_method in ['utorrent', 'transmission', 'deluge', 'blackhole']:
scriptlogger.error(u'Unknown Torrent Method. Aborting!')
print u'Unknown Torrent Method. Aborting!'
print(u'Unknown Torrent Method. Aborting!')
time.sleep(3)
sys.exit()
@ -141,13 +144,13 @@ def main():
if dirName is None:
scriptlogger.error(u'MediaToSickbeard script need a dir to be run. Aborting!')
print u'MediaToSickbeard script need a dir to be run. Aborting!'
print(u'MediaToSickbeard script need a dir to be run. Aborting!')
time.sleep(3)
sys.exit()
if not os.path.isdir(dirName):
scriptlogger.error(u'Folder ' + dirName + ' does not exist. Aborting AutoPostProcess.')
print u'Folder ' + dirName + ' does not exist. Aborting AutoPostProcess.'
print(u'Folder ' + dirName + ' does not exist. Aborting AutoPostProcess.')
time.sleep(3)
sys.exit()
@ -174,26 +177,26 @@ def main():
login_url = protocol + host + ':' + port + web_root + '/login'
scriptlogger.debug('Opening URL: ' + url + ' with params=' + str(params))
print 'Opening URL: ' + url + ' with params=' + str(params)
print('Opening URL: ' + url + ' with params=' + str(params))
try:
sess = requests.Session()
sess.post(login_url, data={'username': username, 'password': password}, stream=True, verify=False)
response = sess.get(url, auth=(username, password), params=params, verify=False, allow_redirects=False)
except Exception, e:
except Exception as e:
scriptlogger.error(u': Unknown exception raised when opening url: ' + str(e))
time.sleep(3)
sys.exit()
if response.status_code == 401:
scriptlogger.error(u'Verify and use correct username and password in autoProcessTV.cfg')
print 'Verify and use correct username and password in autoProcessTV.cfg'
print('Verify and use correct username and password in autoProcessTV.cfg')
time.sleep(3)
sys.exit()
if response.status_code == 200:
scriptlogger.info(u'Script ' + __file__ + ' Succesfull')
print 'Script ' + __file__ + ' Succesfull'
print('Script ' + __file__ + ' Succesfull')
time.sleep(3)
sys.exit()

View file

@ -19,11 +19,12 @@
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import sys
import autoProcessTV
if len(sys.argv) < 2:
print 'No folder supplied - is this being called from SABnzbd?'
print('No folder supplied - is this being called from SABnzbd?')
sys.exit()
elif len(sys.argv) >= 8:
autoProcessTV.processEpisode(sys.argv[1], sys.argv[2], sys.argv[7])

View file

@ -1,13 +1,17 @@
from distutils.core import setup
import py2exe, sys, shutil
import sys
import shutil
try:
import py2exe
except:
pass
sys.argv.append('py2exe')
setup(
options = {'py2exe': {'bundle_files': 1}},
# windows = [{'console': "sabToSickbeard.py"}],
zipfile = None,
console = ['sabToSickbeard.py'],
)
setup(options={'py2exe': {'bundle_files': 1}},
# windows = [{'console': "sabToSickbeard.py"}],
zipfile=None,
console=['sabToSickbeard.py']
)
shutil.copy('dist/sabToSickbeard.exe', '.')

View file

@ -1,125 +0,0 @@
### Questions about SickGear?
To get your questions answered, please ask in the [SickGear Forum], on IRC \#SickGear pn freenode.net, or webchat.
# Contributing to SickGear
1. [Getting Involved](#getting-involved)
2. [How To Report Bugs](#how-to-report-bugs)
3. [Tips For Submitting Code](#tips-for-submitting-code)
## Getting Involved
There are a number of ways to get involved with the development of SickGear. Even if you've never contributed code to an Open Source project before, we're always looking for help identifying bugs, cleaning up code, writing documentation and testing.
The goal of this guide is to provide the best way to contribute to the official SickGear repository. Please read through the full guide detailing [How to Report Bugs](#how-to-report-bugs).
## Discussion
### Issues and IRC
If you think you've found a bug please [file it in the bug tracker](#how-to-report-bugs).
Additionally most of the SickGear development team can be found in the [#SickGear](http://webchat.freenode.net/?channels=SickGear) IRC channel on irc.freenode.net.
## How to Report Bugs
### Make sure it is a SickGear bug
Many bugs reported are actually issues with the user mis-understanding of how something works (there are a bit of moving parts to an ideal setup) and most of the time can be fixed by just changing some settings to fit the users needs.
If you are new to SickGear, it is usually a much better idea to ask for help first in the [SickGear IRC channel](http://webchat.freenode.net/?channels=SickGear). You will get much quicker support, and you will help avoid tying up the SickGear team with invalid bug reports.
### Try the latest version of SickGear
Bugs in old versions of SickGear may have already been fixed. In order to avoid reporting known issues, make sure you are always testing against the latest build/source. Also, we put new code in the `dev` branch first before pushing down to the `master` branch (which is what the binary builds are built off of).
## Tips For Submitting Code
### Code
**NEVER write your patches to the master branch** - it gets messy (I say this from experience!)
**ALWAYS USE A "TOPIC" BRANCH!** Personally I like the `branch-feature_name` format that way its easy to identify the branch and feature at a glance. Also please make note of any forum post / issue number in the pull commit so we know what you are solving (it helps with cleaning up the related items later).
Please follow these guidelines before reporting a bug:
1. **Update to the latest version** &mdash; Check if you can reproduce the issue with the latest version from the `dev` branch.
2. **Use the SickGear Forums search** &mdash; check if the issue has already been reported. If it has been, please comment on the existing issue.
3. **Provide a means to reproduce the problem** &mdash; Please provide as much details as possible, e.g. SickGear log files (obfuscate apikey/passwords), browser and operating system versions, how you started SickGear, and of course the steps to reproduce the problem. Bugs are always reported in the forums.
### Feature requests
Please follow the bug guidelines above for feature requests, i.e. update to the latest version and search for existing issues before posting a new request. You can submit Feature Requests in the [SickGear Forum] as well.
### Pull requests
[Pull requests](https://help.github.com/articles/using-pull-requests) are welcome and the preferred way of accepting code contributions.
Please follow these guidelines before sending a pull request:
1. Update your fork to the latest upstream version.
2. Use the `dev` branch to base your code off of. Create a topic-branch for your work. We will not merge your 'dev' branch, or your 'master' branch, only topic branches, coming from dev are merged.
3. Follow the coding conventions of the original repository. Do not change line endings of the existing file, as this will rewrite the file and loses history.
4. Keep your commits as autonomous as possible, i.e. create a new commit for every single bug fix or feature added.
5. Always add meaningful commit messages. We should not have to guess at what your code is supposed to do.
6. One pull request per feature. If you want multiple features, send multiple PR's
Please follow this process; it's the best way to get your work included in the project:
- [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
and configure the remotes:
```bash
# clone your fork of the repo into the current directory in terminal
git clone git@github.com:<your username>/SickGear.git
# navigate to the newly cloned directory
cd SickGear
# assign the original repo to a remote called "upstream"
git remote add upstream https://github.com/SickGear/SickGear.git
```
- If you cloned a while ago, get the latest changes from upstream:
```bash
# fetch upstream changes
git fetch upstream
# make sure you are on your 'master' branch
git checkout master
# merge upstream changes
git merge upstream/master
```
- Create a new topic branch to contain your feature, change, or fix:
```bash
git checkout -b <topic-branch-name> dev
```
- Commit your changes in logical chunks. or your pull request is unlikely
be merged into the main project. Use git's
[interactive rebase](https://help.github.com/articles/interactive-rebase)
feature to tidy up your commits before making them public.
- Push your topic branch up to your fork:
```bash
git push origin <topic-branch-name>
```
- [Open a Pull Request](https://help.github.com/articles/using-pull-requests) with a
clear title and description.

View file

@ -507,6 +507,7 @@ inc_bottom.tmpl
width:100%;
padding:20px 0;
text-align:center;
clear:both;
font-size:12px
}
@ -1009,6 +1010,10 @@ div.formpaginate{
font-weight:900
}
#addShowForm #blackwhitelist{
padding:0 0 0 15px
}
#addShowForm #blackwhitelist,
#addShowForm #blackwhitelist h4,
#addShowForm #blackwhitelist p{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,011 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

View file

@ -41,7 +41,7 @@
<label class="cleafix" for="use_xbmc">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_xbmc" id="use_xbmc" #if $sickbeard.USE_XBMC then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_xbmc" id="use_xbmc" #if $sickbeard.USE_XBMC then 'checked="checked"' else ''# />
<p>should SickGear send XBMC commands ?</p>
</span>
</label>
@ -52,7 +52,7 @@
<label for="xbmc_always_on">
<span class="component-title">Always on</span>
<span class="component-desc">
<input type="checkbox" name="xbmc_always_on" id="xbmc_always_on" #if $sickbeard.XBMC_ALWAYS_ON then "checked=\"checked\"" else ""# />
<input type="checkbox" name="xbmc_always_on" id="xbmc_always_on" #if $sickbeard.XBMC_ALWAYS_ON then 'checked="checked"' else ''# />
<p>log errors when unreachable ?</p>
</span>
</label>
@ -61,7 +61,7 @@
<label for="xbmc_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="xbmc_notify_onsnatch" id="xbmc_notify_onsnatch" #if $sickbeard.XBMC_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="xbmc_notify_onsnatch" id="xbmc_notify_onsnatch" #if $sickbeard.XBMC_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -70,7 +70,7 @@
<label for="xbmc_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="xbmc_notify_ondownload" id="xbmc_notify_ondownload" #if $sickbeard.XBMC_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="xbmc_notify_ondownload" id="xbmc_notify_ondownload" #if $sickbeard.XBMC_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -79,7 +79,7 @@
<label for="xbmc_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="xbmc_notify_onsubtitledownload" id="xbmc_notify_onsubtitledownload" #if $sickbeard.XBMC_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="xbmc_notify_onsubtitledownload" id="xbmc_notify_onsubtitledownload" #if $sickbeard.XBMC_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -88,7 +88,7 @@
<label for="xbmc_update_library">
<span class="component-title">Update library</span>
<span class="component-desc">
<input type="checkbox" name="xbmc_update_library" id="xbmc_update_library" #if $sickbeard.XBMC_UPDATE_LIBRARY then "checked=\"checked\"" else ""# />
<input type="checkbox" name="xbmc_update_library" id="xbmc_update_library" #if $sickbeard.XBMC_UPDATE_LIBRARY then 'checked="checked"' else ''# />
<p>update XBMC library when a download finishes ?</p>
</span>
</label>
@ -97,7 +97,7 @@
<label for="xbmc_update_full">
<span class="component-title">Full library update</span>
<span class="component-desc">
<input type="checkbox" name="xbmc_update_full" id="xbmc_update_full" #if $sickbeard.XBMC_UPDATE_FULL then "checked=\"checked\"" else ""# />
<input type="checkbox" name="xbmc_update_full" id="xbmc_update_full" #if $sickbeard.XBMC_UPDATE_FULL then 'checked="checked"' else ''# />
<p>perform a full library update if update per-show fails ?</p>
</span>
</label>
@ -106,7 +106,7 @@
<label for="xbmc_update_onlyfirst">
<span class="component-title">Only update first host</span>
<span class="component-desc">
<input type="checkbox" name="xbmc_update_onlyfirst" id="xbmc_update_onlyfirst" #if $sickbeard.XBMC_UPDATE_ONLYFIRST then "checked=\"checked\"" else ""# />
<input type="checkbox" name="xbmc_update_onlyfirst" id="xbmc_update_onlyfirst" #if $sickbeard.XBMC_UPDATE_ONLYFIRST then 'checked="checked"' else ''# />
<p>only send library updates to the first active host ?</p>
</span>
</label>
@ -165,7 +165,7 @@
<label class="cleafix" for="use_kodi">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_kodi" id="use_kodi" #if $sickbeard.USE_KODI then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_kodi" id="use_kodi" #if $sickbeard.USE_KODI then 'checked="checked"' else ''# />
<p>should SickGear send Kodi commands ?</p>
</span>
</label>
@ -175,7 +175,7 @@
<label for="kodi_always_on">
<span class="component-title">Always on</span>
<span class="component-desc">
<input type="checkbox" name="kodi_always_on" id="kodi_always_on" #if $sickbeard.KODI_ALWAYS_ON then "checked=\"checked\"" else ""# />
<input type="checkbox" name="kodi_always_on" id="kodi_always_on" #if $sickbeard.KODI_ALWAYS_ON then 'checked="checked"' else ''# />
<p>log errors when unreachable ?</p>
</span>
</label>
@ -184,7 +184,7 @@
<label for="kodi_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="kodi_notify_onsnatch" id="kodi_notify_onsnatch" #if $sickbeard.KODI_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="kodi_notify_onsnatch" id="kodi_notify_onsnatch" #if $sickbeard.KODI_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -193,7 +193,7 @@
<label for="kodi_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="kodi_notify_ondownload" id="kodi_notify_ondownload" #if $sickbeard.KODI_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="kodi_notify_ondownload" id="kodi_notify_ondownload" #if $sickbeard.KODI_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -202,7 +202,7 @@
<label for="kodi_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="kodi_notify_onsubtitledownload" id="kodi_notify_onsubtitledownload" #if $sickbeard.KODI_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="kodi_notify_onsubtitledownload" id="kodi_notify_onsubtitledownload" #if $sickbeard.KODI_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -211,7 +211,7 @@
<label for="kodi_update_library">
<span class="component-title">Update library</span>
<span class="component-desc">
<input type="checkbox" name="kodi_update_library" id="kodi_update_library" #if $sickbeard.KODI_UPDATE_LIBRARY then "checked=\"checked\"" else ""# />
<input type="checkbox" name="kodi_update_library" id="kodi_update_library" #if $sickbeard.KODI_UPDATE_LIBRARY then 'checked="checked"' else ''# />
<p>update Kodi library when a download finishes ?</p>
</span>
</label>
@ -220,7 +220,7 @@
<label for="kodi_update_full">
<span class="component-title">Full library update</span>
<span class="component-desc">
<input type="checkbox" name="kodi_update_full" id="kodi_update_full" #if $sickbeard.KODI_UPDATE_FULL then "checked=\"checked\"" else ""# />
<input type="checkbox" name="kodi_update_full" id="kodi_update_full" #if $sickbeard.KODI_UPDATE_FULL then 'checked="checked"' else ''# />
<p>perform a full library update if update per-show fails ?</p>
</span>
</label>
@ -229,7 +229,7 @@
<label for="kodi_update_onlyfirst">
<span class="component-title">Only update first host</span>
<span class="component-desc">
<input type="checkbox" name="kodi_update_onlyfirst" id="kodi_update_onlyfirst" #if $sickbeard.KODI_UPDATE_ONLYFIRST then "checked=\"checked\"" else ""# />
<input type="checkbox" name="kodi_update_onlyfirst" id="kodi_update_onlyfirst" #if $sickbeard.KODI_UPDATE_ONLYFIRST then 'checked="checked"' else ''# />
<p>only send library updates to the first active host ?</p>
</span>
</label>
@ -352,7 +352,7 @@
<label for="plex_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="plex_notify_onsnatch" id="plex_notify_onsnatch" #if $sickbeard.PLEX_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="plex_notify_onsnatch" id="plex_notify_onsnatch" #if $sickbeard.PLEX_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>download start notification</p>
</span>
</label>
@ -361,7 +361,7 @@
<label for="plex_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="plex_notify_ondownload" id="plex_notify_ondownload" #if $sickbeard.PLEX_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="plex_notify_ondownload" id="plex_notify_ondownload" #if $sickbeard.PLEX_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>download finish notification</p>
</span>
</label>
@ -370,7 +370,7 @@
<label for="plex_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="plex_notify_onsubtitledownload" id="plex_notify_onsubtitledownload" #if $sickbeard.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="plex_notify_onsubtitledownload" id="plex_notify_onsubtitledownload" #if $sickbeard.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>subtitle downloaded notification</p>
</span>
</label>
@ -411,7 +411,7 @@
<label for="use_nmj">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_nmj" id="use_nmj" #if $sickbeard.USE_NMJ then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_nmj" id="use_nmj" #if $sickbeard.USE_NMJ then 'checked="checked"' else ''# />
<p>should SickGear send update commands to NMJ ?</p>
</span>
</label>
@ -441,7 +441,7 @@
<div class="field-pair">
<label for="nmj_database">
<span class="component-title">NMJ database</span>
<input type="text" name="nmj_database" id="nmj_database" value="$sickbeard.NMJ_DATABASE" class="form-control input-sm input250" #if $sickbeard.NMJ_DATABASE then "readonly=\"readonly\"" else ""# />
<input type="text" name="nmj_database" id="nmj_database" value="$sickbeard.NMJ_DATABASE" class="form-control input-sm input250" #if $sickbeard.NMJ_DATABASE then "readonly=\"readonly\"" else ''# />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -451,7 +451,7 @@
<div class="field-pair">
<label for="nmj_mount">
<span class="component-title">NMJ mount url</span>
<input type="text" name="nmj_mount" id="nmj_mount" value="$sickbeard.NMJ_MOUNT" class="form-control input-sm input250" #if $sickbeard.NMJ_MOUNT then "readonly=\"readonly\"" else ""# />
<input type="text" name="nmj_mount" id="nmj_mount" value="$sickbeard.NMJ_MOUNT" class="form-control input-sm input250" #if $sickbeard.NMJ_MOUNT then "readonly=\"readonly\"" else ''# />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -477,7 +477,7 @@
<label for="use_nmjv2">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_nmjv2" id="use_nmjv2" #if $sickbeard.USE_NMJv2 then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_nmjv2" id="use_nmjv2" #if $sickbeard.USE_NMJv2 then 'checked="checked"' else ''# />
<p>should SickGear send update commands to NMJv2 ?</p>
</span>
</label>
@ -498,10 +498,10 @@
<span class="component-title">Database location</span>
<span class="component-desc">
<label for="NMJV2_DBLOC_A" class="space-right">
<input type="radio" NAME="nmjv2_dbloc" VALUE="local" id="NMJV2_DBLOC_A" #if $sickbeard.NMJv2_DBLOC=="local" then "checked=\"checked\"" else ""# />PCH Local Media
<input type="radio" NAME="nmjv2_dbloc" VALUE="local" id="NMJV2_DBLOC_A" #if $sickbeard.NMJv2_DBLOC=='local' then 'checked="checked"' else ''# />PCH Local Media
</label>
<label for="NMJV2_DBLOC_B">
<input type="radio" NAME="nmjv2_dbloc" VALUE="network" id="NMJV2_DBLOC_B" #if $sickbeard.NMJv2_DBLOC=="network" then "checked=\"checked\"" else ""#/>PCH Network Media
<input type="radio" NAME="nmjv2_dbloc" VALUE="network" id="NMJV2_DBLOC_B" #if $sickbeard.NMJv2_DBLOC=='network' then 'checked="checked"' else ''#/>PCH Network Media
</label>
</span>
</div>
@ -538,7 +538,7 @@
<div class="field-pair">
<label for="nmjv2_database">
<span class="component-title">NMJv2 database</span>
<input type="text" name="nmjv2_database" id="nmjv2_database" value="$sickbeard.NMJv2_DATABASE" class="form-control input-sm input250" #if $sickbeard.NMJv2_DATABASE then "readonly=\"readonly\"" else ""# />
<input type="text" name="nmjv2_database" id="nmjv2_database" value="$sickbeard.NMJv2_DATABASE" class="form-control input-sm input250" #if $sickbeard.NMJv2_DATABASE then "readonly=\"readonly\"" else ''# />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -567,7 +567,7 @@
<label for="use_synoindex">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_synoindex" id="use_synoindex" #if $sickbeard.USE_SYNOINDEX then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_synoindex" id="use_synoindex" #if $sickbeard.USE_SYNOINDEX then 'checked="checked"' else ''# />
<p>should SickGear send Synology notifications ?</p>
</span>
</label>
@ -597,7 +597,7 @@
<label for="use_synologynotifier">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_synologynotifier" id="use_synologynotifier" #if $sickbeard.USE_SYNOLOGYNOTIFIER then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_synologynotifier" id="use_synologynotifier" #if $sickbeard.USE_SYNOLOGYNOTIFIER then 'checked="checked"' else ''# />
<p>should SickGear send notifications to the Synology Notifier ?</p>
</span>
</label>
@ -611,7 +611,7 @@
<label for="synologynotifier_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="synologynotifier_notify_onsnatch" id="synologynotifier_notify_onsnatch" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="synologynotifier_notify_onsnatch" id="synologynotifier_notify_onsnatch" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -620,7 +620,7 @@
<label for="synologynotifier_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="synologynotifier_notify_ondownload" id="synologynotifier_notify_ondownload" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="synologynotifier_notify_ondownload" id="synologynotifier_notify_ondownload" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -629,7 +629,7 @@
<label for="synologynotifier_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="synologynotifier_notify_onsubtitledownload" id="synologynotifier_notify_onsubtitledownload" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="synologynotifier_notify_onsubtitledownload" id="synologynotifier_notify_onsubtitledownload" #if $sickbeard.SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -651,7 +651,7 @@
<label for="use_pytivo">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_pytivo" id="use_pytivo" #if $sickbeard.USE_PYTIVO then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_pytivo" id="use_pytivo" #if $sickbeard.USE_PYTIVO then 'checked="checked"' else ''# />
<p>should SickGear send notifications to pyTivo ?</p>
</span>
</label>
@ -713,7 +713,7 @@
<label for="use_growl">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_growl" id="use_growl" #if $sickbeard.USE_GROWL then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_growl" id="use_growl" #if $sickbeard.USE_GROWL then 'checked="checked"' else ''# />
<p>should SickGear send Growl notifications ?</p>
</span>
</label>
@ -724,7 +724,7 @@
<label for="growl_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="growl_notify_onsnatch" id="growl_notify_onsnatch" #if $sickbeard.GROWL_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="growl_notify_onsnatch" id="growl_notify_onsnatch" #if $sickbeard.GROWL_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -733,7 +733,7 @@
<label for="growl_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="growl_notify_ondownload" id="growl_notify_ondownload" #if $sickbeard.GROWL_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="growl_notify_ondownload" id="growl_notify_ondownload" #if $sickbeard.GROWL_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -742,7 +742,7 @@
<label for="growl_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="growl_notify_onsubtitledownload" id="growl_notify_onsubtitledownload" #if $sickbeard.GROWL_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="growl_notify_onsubtitledownload" id="growl_notify_onsubtitledownload" #if $sickbeard.GROWL_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -791,7 +791,7 @@
<label for="use_prowl">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_prowl" id="use_prowl" #if $sickbeard.USE_PROWL then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_prowl" id="use_prowl" #if $sickbeard.USE_PROWL then 'checked="checked"' else ''# />
<p>should SickGear send Prowl notifications ?</p>
</span>
</label>
@ -802,7 +802,7 @@
<label for="prowl_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="prowl_notify_onsnatch" id="prowl_notify_onsnatch" #if $sickbeard.PROWL_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="prowl_notify_onsnatch" id="prowl_notify_onsnatch" #if $sickbeard.PROWL_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -811,7 +811,7 @@
<label for="prowl_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="prowl_notify_ondownload" id="prowl_notify_ondownload" #if $sickbeard.PROWL_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="prowl_notify_ondownload" id="prowl_notify_ondownload" #if $sickbeard.PROWL_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -820,7 +820,7 @@
<label for="prowl_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="prowl_notify_onsubtitledownload" id="prowl_notify_onsubtitledownload" #if $sickbeard.PROWL_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="prowl_notify_onsubtitledownload" id="prowl_notify_onsubtitledownload" #if $sickbeard.PROWL_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -839,11 +839,11 @@
<label for="prowl_priority">
<span class="component-title">Prowl priority:</span>
<select id="prowl_priority" name="prowl_priority" class="form-control input-sm">
<option value="-2" #if $sickbeard.PROWL_PRIORITY == "-2" then 'selected="selected"' else ""#>Very Low</option>
<option value="-1" #if $sickbeard.PROWL_PRIORITY == "-1" then 'selected="selected"' else ""#>Moderate</option>
<option value="0" #if $sickbeard.PROWL_PRIORITY == "0" then 'selected="selected"' else ""#>Normal</option>
<option value="1" #if $sickbeard.PROWL_PRIORITY == "1" then 'selected="selected"' else ""#>High</option>
<option value="2" #if $sickbeard.PROWL_PRIORITY == "2" then 'selected="selected"' else ""#>Emergency</option>
<option value="-2" #if $sickbeard.PROWL_PRIORITY == '-2' then 'selected="selected"' else ''#>Very Low</option>
<option value="-1" #if $sickbeard.PROWL_PRIORITY == '-1' then 'selected="selected"' else ''#>Moderate</option>
<option value="0" #if $sickbeard.PROWL_PRIORITY == '0' then 'selected="selected"' else ''#>Normal</option>
<option value="1" #if $sickbeard.PROWL_PRIORITY == '1' then 'selected="selected"' else ''#>High</option>
<option value="2" #if $sickbeard.PROWL_PRIORITY == '2' then 'selected="selected"' else ''#>Emergency</option>
</select>
</label>
<label>
@ -871,7 +871,7 @@
<label for="use_libnotify">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_libnotify" id="use_libnotify" #if $sickbeard.USE_LIBNOTIFY then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_libnotify" id="use_libnotify" #if $sickbeard.USE_LIBNOTIFY then 'checked="checked"' else ''# />
<p>should SickGear send Libnotify notifications ?</p>
</span>
</label>
@ -882,7 +882,7 @@
<label for="libnotify_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="libnotify_notify_onsnatch" id="libnotify_notify_onsnatch" #if $sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="libnotify_notify_onsnatch" id="libnotify_notify_onsnatch" #if $sickbeard.LIBNOTIFY_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -891,7 +891,7 @@
<label for="libnotify_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="libnotify_notify_ondownload" id="libnotify_notify_ondownload" #if $sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="libnotify_notify_ondownload" id="libnotify_notify_ondownload" #if $sickbeard.LIBNOTIFY_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -900,7 +900,7 @@
<label for="libnotify_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="libnotify_notify_onsubtitledownload" id="libnotify_notify_onsubtitledownload" #if $sickbeard.LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="libnotify_notify_onsubtitledownload" id="libnotify_notify_onsubtitledownload" #if $sickbeard.LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -925,7 +925,7 @@
<label for="use_pushover">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_pushover" id="use_pushover" #if $sickbeard.USE_PUSHOVER then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_pushover" id="use_pushover" #if $sickbeard.USE_PUSHOVER then 'checked="checked"' else ''# />
<p>should SickGear send Pushover notifications ?</p>
</span>
</label>
@ -936,7 +936,7 @@
<label for="pushover_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="pushover_notify_onsnatch" id="pushover_notify_onsnatch" #if $sickbeard.PUSHOVER_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushover_notify_onsnatch" id="pushover_notify_onsnatch" #if $sickbeard.PUSHOVER_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -945,7 +945,7 @@
<label for="pushover_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="pushover_notify_ondownload" id="pushover_notify_ondownload" #if $sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushover_notify_ondownload" id="pushover_notify_ondownload" #if $sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -954,7 +954,7 @@
<label for="pushover_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="pushover_notify_onsubtitledownload" id="pushover_notify_onsubtitledownload" #if $sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushover_notify_onsubtitledownload" id="pushover_notify_onsubtitledownload" #if $sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -983,10 +983,10 @@
<label for="pushover_priority">
<span class="component-title">Pushover priority:</span>
<select id="pushover_priority" name="pushover_priority" class="form-control input-sm">
<option value="-2" #if $sickbeard.PUSHOVER_PRIORITY == -2 then 'selected="selected"' else ""#>Lowest</option>
<option value="-1" #if $sickbeard.PUSHOVER_PRIORITY == -1 then 'selected="selected"' else ""#>Low</option>
<option value="0" #if $sickbeard.PUSHOVER_PRIORITY == 0 then 'selected="selected"' else ""#>Normal</option>
<option value="1" #if $sickbeard.PUSHOVER_PRIORITY == 1 then 'selected="selected"' else ""#>High</option>
<option value="-2" #if $sickbeard.PUSHOVER_PRIORITY == '-2' then 'selected="selected"' else ''#>Lowest</option>
<option value="-1" #if $sickbeard.PUSHOVER_PRIORITY == '-1' then 'selected="selected"' else ''#>Low</option>
<option value="0" #if $sickbeard.PUSHOVER_PRIORITY == '0' then 'selected="selected"' else ''#>Normal</option>
<option value="1" #if $sickbeard.PUSHOVER_PRIORITY == '1' then 'selected="selected"' else ''#>High</option>
</select>
</label>
<label>
@ -1015,28 +1015,28 @@
<label for="pushover_sound">
<span class="component-title">Pushover sound</span>
<select id="pushover_sound" name="pushover_sound" class="form-control input-sm">
<option value="pushover" #if $sickbeard.PUSHOVER_SOUND == "pushover" then 'selected="selected"' else ""#>Pushover (default)</option>
<option value="bike" #if $sickbeard.PUSHOVER_SOUND == "bike" then 'selected="selected"' else ""#>Bike</option>
<option value="bugle" #if $sickbeard.PUSHOVER_SOUND == "bugle" then 'selected="selected"' else ""#>Bugle</option>
<option value="cashregister" #if $sickbeard.PUSHOVER_SOUND == "cashregister" then 'selected="selected"' else ""#>Cash Register</option>
<option value="classical" #if $sickbeard.PUSHOVER_SOUND == "classical" then 'selected="selected"' else ""#>Classical</option>
<option value="cosmic" #if $sickbeard.PUSHOVER_SOUND == "cosmic" then 'selected="selected"' else ""#>Cosmic</option>
<option value="falling" #if $sickbeard.PUSHOVER_SOUND == "falling" then 'selected="selected"' else ""#>Falling</option>
<option value="gamelan" #if $sickbeard.PUSHOVER_SOUND == "gamelan" then 'selected="selected"' else ""#>Gamelan</option>
<option value="incoming" #if $sickbeard.PUSHOVER_SOUND == "incoming" then 'selected="selected"' else ""#>Incoming</option>
<option value="intermission" #if $sickbeard.PUSHOVER_SOUND == "intermission" then 'selected="selected"' else ""#>Intermission</option>
<option value="magic" #if $sickbeard.PUSHOVER_SOUND == "magic" then 'selected="selected"' else ""#>Magic</option>
<option value="mechanical" #if $sickbeard.PUSHOVER_SOUND == "mechanical" then 'selected="selected"' else ""#>Mechanical</option>
<option value="pianobar" #if $sickbeard.PUSHOVER_SOUND == "pianobar" then 'selected="selected"' else ""#>Piano Bar</option>
<option value="siren" #if $sickbeard.PUSHOVER_SOUND == "siren" then 'selected="selected"' else ""#>Siren</option>
<option value="spacealarm" #if $sickbeard.PUSHOVER_SOUND == "spacealarm" then 'selected="selected"' else ""#>Space Alarm</option>
<option value="tugboat" #if $sickbeard.PUSHOVER_SOUND == "tugboat" then 'selected="selected"' else ""#>Tug Boat</option>
<option value="alien" #if $sickbeard.PUSHOVER_SOUND == "alien" then 'selected="selected"' else ""#>Alien Alarm (long)</option>
<option value="climb" #if $sickbeard.PUSHOVER_SOUND == "climb" then 'selected="selected"' else ""#>Climb (long)</option>
<option value="persistent" #if $sickbeard.PUSHOVER_SOUND == "persistent" then 'selected="selected"' else ""#>Persistent (long)</option>
<option value="echo" #if $sickbeard.PUSHOVER_SOUND == "echo" then 'selected="selected"' else ""#>Pushover Echo (long)</option>
<option value="updown" #if $sickbeard.PUSHOVER_SOUND == "updown" then 'selected="selected"' else ""#>Up Down (long)</option>
<option value="none" #if $sickbeard.PUSHOVER_SOUND == "none" then 'selected="selected"' else ""#>None (silent)</option>
<option value="pushover" #if $sickbeard.PUSHOVER_SOUND == 'pushover' then 'selected="selected"' else ''#>Pushover (default)</option>
<option value="bike" #if $sickbeard.PUSHOVER_SOUND == 'bike' then 'selected="selected"' else ''#>Bike</option>
<option value="bugle" #if $sickbeard.PUSHOVER_SOUND == 'bugle' then 'selected="selected"' else ''#>Bugle</option>
<option value="cashregister" #if $sickbeard.PUSHOVER_SOUND == 'cashregister' then 'selected="selected"' else ''#>Cash Register</option>
<option value="classical" #if $sickbeard.PUSHOVER_SOUND == 'classical' then 'selected="selected"' else ''#>Classical</option>
<option value="cosmic" #if $sickbeard.PUSHOVER_SOUND == 'cosmic' then 'selected="selected"' else ''#>Cosmic</option>
<option value="falling" #if $sickbeard.PUSHOVER_SOUND == 'falling' then 'selected="selected"' else ''#>Falling</option>
<option value="gamelan" #if $sickbeard.PUSHOVER_SOUND == 'gamelan' then 'selected="selected"' else ''#>Gamelan</option>
<option value="incoming" #if $sickbeard.PUSHOVER_SOUND == 'incoming' then 'selected="selected"' else ''#>Incoming</option>
<option value="intermission" #if $sickbeard.PUSHOVER_SOUND == 'intermission' then 'selected="selected"' else ''#>Intermission</option>
<option value="magic" #if $sickbeard.PUSHOVER_SOUND == 'magic' then 'selected="selected"' else ''#>Magic</option>
<option value="mechanical" #if $sickbeard.PUSHOVER_SOUND == 'mechanical' then 'selected="selected"' else ''#>Mechanical</option>
<option value="pianobar" #if $sickbeard.PUSHOVER_SOUND == 'pianobar' then 'selected="selected"' else ''#>Piano Bar</option>
<option value="siren" #if $sickbeard.PUSHOVER_SOUND == 'siren' then 'selected="selected"' else ''#>Siren</option>
<option value="spacealarm" #if $sickbeard.PUSHOVER_SOUND == 'spacealarm' then 'selected="selected"' else ''#>Space Alarm</option>
<option value="tugboat" #if $sickbeard.PUSHOVER_SOUND == 'tugboat' then 'selected="selected"' else ''#>Tug Boat</option>
<option value="alien" #if $sickbeard.PUSHOVER_SOUND == 'alien' then 'selected="selected"' else ''#>Alien Alarm (long)</option>
<option value="climb" #if $sickbeard.PUSHOVER_SOUND == 'climb' then 'selected="selected"' else ''#>Climb (long)</option>
<option value="persistent" #if $sickbeard.PUSHOVER_SOUND == 'persistent' then 'selected="selected"' else ''#>Persistent (long)</option>
<option value="echo" #if $sickbeard.PUSHOVER_SOUND == 'echo' then 'selected="selected"' else ''#>Pushover Echo (long)</option>
<option value="updown" #if $sickbeard.PUSHOVER_SOUND == 'updown' then 'selected="selected"' else ''#>Up Down (long)</option>
<option value="none" #if $sickbeard.PUSHOVER_SOUND == 'none' then 'selected="selected"' else ''#>None (silent)</option>
</select>
</label>
<label>
@ -1063,7 +1063,7 @@
<label for="use_boxcar2">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_boxcar2" id="use_boxcar2" #if $sickbeard.USE_BOXCAR2 then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_boxcar2" id="use_boxcar2" #if $sickbeard.USE_BOXCAR2 then 'checked="checked"' else ''# />
<p>should SickGear send Boxcar2 notifications ?</p>
</span>
</label>
@ -1074,7 +1074,7 @@
<label for="boxcar2_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="boxcar2_notify_onsnatch" id="boxcar2_notify_onsnatch" #if $sickbeard.BOXCAR2_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="boxcar2_notify_onsnatch" id="boxcar2_notify_onsnatch" #if $sickbeard.BOXCAR2_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -1083,7 +1083,7 @@
<label for="boxcar2_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="boxcar2_notify_ondownload" id="boxcar2_notify_ondownload" #if $sickbeard.BOXCAR2_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="boxcar2_notify_ondownload" id="boxcar2_notify_ondownload" #if $sickbeard.BOXCAR2_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -1092,7 +1092,7 @@
<label for="boxcar2_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="boxcar2_notify_onsubtitledownload" id="boxcar2_notify_onsubtitledownload" #if $sickbeard.BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="boxcar2_notify_onsubtitledownload" id="boxcar2_notify_onsubtitledownload" #if $sickbeard.BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -1111,36 +1111,36 @@
<label for="boxcar2_sound">
<span class="component-title">Custom sound</span>
<select id="boxcar2_sound" name="boxcar2_sound" class="form-control input-sm">
<option value="default" #if $sickbeard.BOXCAR2_SOUND == "default" then 'selected="selected"' else ""#>Default (General)</option>
<option value="no-sound" #if $sickbeard.BOXCAR2_SOUND == "no-sound" then 'selected="selected"' else ""#>Silent</option>
<option value="beep-crisp" #if $sickbeard.BOXCAR2_SOUND == "beep-crisp" then 'selected="selected"' else ""#>Beep Crisp</option>
<option value="beep-soft" #if $sickbeard.BOXCAR2_SOUND == "beep-soft" then 'selected="selected"' else ""#>Beep Soft</option>
<option value="bell-modern" #if $sickbeard.BOXCAR2_SOUND == "bell-modern" then 'selected="selected"' else ""#>Bell Modern</option>
<option value="bell-one-tone" #if $sickbeard.BOXCAR2_SOUND == "bell-one-tone" then 'selected="selected"' else ""#>Bell One Tone</option>
<option value="bell-simple" #if $sickbeard.BOXCAR2_SOUND == "bell-simple" then 'selected="selected"' else ""#>Bell Simple</option>
<option value="bell-triple" #if $sickbeard.BOXCAR2_SOUND == "bell-triple" then 'selected="selected"' else ""#>Bell Triple</option>
<option value="bird-1" #if $sickbeard.BOXCAR2_SOUND == "bird-1" then 'selected="selected"' else ""#>Bird 1</option>
<option value="bird-2" #if $sickbeard.BOXCAR2_SOUND == "bird-2" then 'selected="selected"' else ""#>Bird 2</option>
<option value="boing" #if $sickbeard.BOXCAR2_SOUND == "boing" then 'selected="selected"' else ""#>Boing</option>
<option value="cash" #if $sickbeard.BOXCAR2_SOUND == "cash" then 'selected="selected"' else ""#>Cash</option>
<option value="clanging" #if $sickbeard.BOXCAR2_SOUND == "clanging" then 'selected="selected"' else ""#>Clanging</option>
<option value="detonator-charge" #if $sickbeard.BOXCAR2_SOUND == "detonator-charge" then 'selected="selected"' else ""#>Detonator Charge</option>
<option value="digital-alarm" #if $sickbeard.BOXCAR2_SOUND == "digital-alarm" then 'selected="selected"' else ""#>Digital Alarm</option>
<option value="done" #if $sickbeard.BOXCAR2_SOUND == "done" then 'selected="selected"' else ""#>Done</option>
<option value="echo" #if $sickbeard.BOXCAR2_SOUND == "echo" then 'selected="selected"' else ""#>Echo</option>
<option value="flourish" #if $sickbeard.BOXCAR2_SOUND == "flourish" then 'selected="selected"' else ""#>Flourish</option>
<option value="harp" #if $sickbeard.BOXCAR2_SOUND == "harp" then 'selected="selected"' else ""#>Harp</option>
<option value="light" #if $sickbeard.BOXCAR2_SOUND == "light" then 'selected="selected"' else ""#>Light</option>
<option value="magic-chime" #if $sickbeard.BOXCAR2_SOUND == "magic-chime" then 'selected="selected"' else ""#>Magic Chime</option>
<option value="magic-coin" #if $sickbeard.BOXCAR2_SOUND == "magic-coin" then 'selected="selected"' else ""#>Magic Coin 1</option>
<option value="notifier-1" #if $sickbeard.BOXCAR2_SOUND == "notifier-1" then 'selected="selected"' else ""#>Notifier 1</option>
<option value="notifier-2" #if $sickbeard.BOXCAR2_SOUND == "notifier-2" then 'selected="selected"' else ""#>Notifier 2</option>
<option value="notifier-3" #if $sickbeard.BOXCAR2_SOUND == "notifier-3" then 'selected="selected"' else ""#>Notifier 3</option>
<option value="orchestral-long" #if $sickbeard.BOXCAR2_SOUND == "orchestral-long" then 'selected="selected"' else ""#>Orchestral Long</option>
<option value="orchestral-short" #if $sickbeard.BOXCAR2_SOUND == "orchestral-short" then 'selected="selected"' else ""#>Orchestral Short</option>
<option value="score" #if $sickbeard.BOXCAR2_SOUND == "score" then 'selected="selected"' else ""#>Score</option>
<option value="success" #if $sickbeard.BOXCAR2_SOUND == "success" then 'selected="selected"' else ""#>Success</option>
<option value="up" #if $sickbeard.BOXCAR2_SOUND == "up" then 'selected="selected"' else ""#>Up</option>
<option value="default" #if $sickbeard.BOXCAR2_SOUND == 'default' then 'selected="selected"' else ''#>Default (General)</option>
<option value="no-sound" #if $sickbeard.BOXCAR2_SOUND == 'no-sound' then 'selected="selected"' else ''#>Silent</option>
<option value="beep-crisp" #if $sickbeard.BOXCAR2_SOUND == 'beep-crisp' then 'selected="selected"' else ''#>Beep Crisp</option>
<option value="beep-soft" #if $sickbeard.BOXCAR2_SOUND == 'beep-soft' then 'selected="selected"' else ''#>Beep Soft</option>
<option value="bell-modern" #if $sickbeard.BOXCAR2_SOUND == 'bell-modern' then 'selected="selected"' else ''#>Bell Modern</option>
<option value="bell-one-tone" #if $sickbeard.BOXCAR2_SOUND == 'bell-one-tone' then 'selected="selected"' else ''#>Bell One Tone</option>
<option value="bell-simple" #if $sickbeard.BOXCAR2_SOUND == 'bell-simple' then 'selected="selected"' else ''#>Bell Simple</option>
<option value="bell-triple" #if $sickbeard.BOXCAR2_SOUND == 'bell-triple' then 'selected="selected"' else ''#>Bell Triple</option>
<option value="bird-1" #if $sickbeard.BOXCAR2_SOUND == 'bird-1' then 'selected="selected"' else ''#>Bird 1</option>
<option value="bird-2" #if $sickbeard.BOXCAR2_SOUND == 'bird-2' then 'selected="selected"' else ''#>Bird 2</option>
<option value="boing" #if $sickbeard.BOXCAR2_SOUND == 'boing' then 'selected="selected"' else ''#>Boing</option>
<option value="cash" #if $sickbeard.BOXCAR2_SOUND == 'cash' then 'selected="selected"' else ''#>Cash</option>
<option value="clanging" #if $sickbeard.BOXCAR2_SOUND == 'clanging' then 'selected="selected"' else ''#>Clanging</option>
<option value="detonator-charge" #if $sickbeard.BOXCAR2_SOUND == 'detonator-charge' then 'selected="selected"' else ''#>Detonator Charge</option>
<option value="digital-alarm" #if $sickbeard.BOXCAR2_SOUND == 'digital-alarm' then 'selected="selected"' else ''#>Digital Alarm</option>
<option value="done" #if $sickbeard.BOXCAR2_SOUND == 'done' then 'selected="selected"' else ''#>Done</option>
<option value="echo" #if $sickbeard.BOXCAR2_SOUND == 'echo' then 'selected="selected"' else ''#>Echo</option>
<option value="flourish" #if $sickbeard.BOXCAR2_SOUND == 'flourish' then 'selected="selected"' else ''#>Flourish</option>
<option value="harp" #if $sickbeard.BOXCAR2_SOUND == 'harp' then 'selected="selected"' else ''#>Harp</option>
<option value="light" #if $sickbeard.BOXCAR2_SOUND == 'light' then 'selected="selected"' else ''#>Light</option>
<option value="magic-chime" #if $sickbeard.BOXCAR2_SOUND == 'magic-chime' then 'selected="selected"' else ''#>Magic Chime</option>
<option value="magic-coin" #if $sickbeard.BOXCAR2_SOUND == 'magic-coin' then 'selected="selected"' else ''#>Magic Coin 1</option>
<option value="notifier-1" #if $sickbeard.BOXCAR2_SOUND == 'notifier-1' then 'selected="selected"' else ''#>Notifier 1</option>
<option value="notifier-2" #if $sickbeard.BOXCAR2_SOUND == 'notifier-2' then 'selected="selected"' else ''#>Notifier 2</option>
<option value="notifier-3" #if $sickbeard.BOXCAR2_SOUND == 'notifier-3' then 'selected="selected"' else ''#>Notifier 3</option>
<option value="orchestral-long" #if $sickbeard.BOXCAR2_SOUND == 'orchestral-long' then 'selected="selected"' else ''#>Orchestral Long</option>
<option value="orchestral-short" #if $sickbeard.BOXCAR2_SOUND == 'orchestral-short' then 'selected="selected"' else ''#>Orchestral Short</option>
<option value="score" #if $sickbeard.BOXCAR2_SOUND == 'score' then 'selected="selected"' else ''#>Score</option>
<option value="success" #if $sickbeard.BOXCAR2_SOUND == 'success' then 'selected="selected"' else ''#>Success</option>
<option value="up" #if $sickbeard.BOXCAR2_SOUND == 'up' then 'selected="selected"' else ''#>Up</option>
</select>
</label>
<label>
@ -1159,7 +1159,7 @@
<div class="component-group">
<div class="component-group-desc">
<img class="notifier-icon" src="$sbRoot/images/notifiers/nma.png" alt="" title="NMA"/>
<h3><a href="<%= anon_url('http://nma.usk.bz') %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;">Notify My Android</a></h3>
<h3><a href="<%= anon_url('https://www.notifymyandroid.com') %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;">Notify My Android</a></h3>
<p>Notify My Android is a Prowl-like Android App and API that offers an easy way to send notifications from your application directly to your Android device.</p>
</div>
<fieldset class="component-group-list">
@ -1167,7 +1167,7 @@
<label for="use_nma">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_nma" id="use_nma" #if $sickbeard.USE_NMA then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_nma" id="use_nma" #if $sickbeard.USE_NMA then 'checked="checked"' else ''# />
<p>should SickGear send NMA notifications ?</p>
</span>
</label>
@ -1178,7 +1178,7 @@
<label for="nma_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="nma_notify_onsnatch" id="nma_notify_onsnatch" #if $sickbeard.NMA_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="nma_notify_onsnatch" id="nma_notify_onsnatch" #if $sickbeard.NMA_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -1187,7 +1187,7 @@
<label for="nma_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="nma_notify_ondownload" id="nma_notify_ondownload" #if $sickbeard.NMA_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="nma_notify_ondownload" id="nma_notify_ondownload" #if $sickbeard.NMA_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -1196,7 +1196,7 @@
<label for="nma_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="nma_notify_onsubtitledownload" id="nma_notify_onsubtitledownload" #if $sickbeard.NMA_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="nma_notify_onsubtitledownload" id="nma_notify_onsubtitledownload" #if $sickbeard.NMA_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -1215,11 +1215,11 @@
<label for="nma_priority">
<span class="component-title">NMA priority:</span>
<select id="nma_priority" name="nma_priority" class="form-control input-sm">
<option value="-2" #if $sickbeard.NMA_PRIORITY == "-2" then 'selected="selected"' else ""#>Very Low</option>
<option value="-1" #if $sickbeard.NMA_PRIORITY == "-1" then 'selected="selected"' else ""#>Moderate</option>
<option value="0" #if $sickbeard.NMA_PRIORITY == "0" then 'selected="selected"' else ""#>Normal</option>
<option value="1" #if $sickbeard.NMA_PRIORITY == "1" then 'selected="selected"' else ""#>High</option>
<option value="2" #if $sickbeard.NMA_PRIORITY == "2" then 'selected="selected"' else ""#>Emergency</option>
<option value="-2" #if $sickbeard.NMA_PRIORITY == '-2' then 'selected="selected"' else ''#>Very Low</option>
<option value="-1" #if $sickbeard.NMA_PRIORITY == '-1' then 'selected="selected"' else ''#>Moderate</option>
<option value="0" #if $sickbeard.NMA_PRIORITY == '0' then 'selected="selected"' else ''#>Normal</option>
<option value="1" #if $sickbeard.NMA_PRIORITY == '1' then 'selected="selected"' else ''#>High</option>
<option value="2" #if $sickbeard.NMA_PRIORITY == '2' then 'selected="selected"' else ''#>Emergency</option>
</select>
</label>
<label>
@ -1246,7 +1246,7 @@
<label for="use_pushalot">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_pushalot" id="use_pushalot" #if $sickbeard.USE_PUSHALOT then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_pushalot" id="use_pushalot" #if $sickbeard.USE_PUSHALOT then 'checked="checked"' else ''# />
<p>should SickGear send Pushalot notifications ?</p>
</span>
</label>
@ -1257,7 +1257,7 @@
<label for="pushalot_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="pushalot_notify_onsnatch" id="pushalot_notify_onsnatch" #if $sickbeard.PUSHALOT_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushalot_notify_onsnatch" id="pushalot_notify_onsnatch" #if $sickbeard.PUSHALOT_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -1266,7 +1266,7 @@
<label for="pushalot_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="pushalot_notify_ondownload" id="pushalot_notify_ondownload" #if $sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushalot_notify_ondownload" id="pushalot_notify_ondownload" #if $sickbeard.PUSHALOT_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -1275,7 +1275,7 @@
<label for="pushalot_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="pushalot_notify_onsubtitledownload" id="pushalot_notify_onsubtitledownload" #if $sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushalot_notify_onsubtitledownload" id="pushalot_notify_onsubtitledownload" #if $sickbeard.PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -1309,7 +1309,7 @@
<label for="use_pushbullet">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_pushbullet" id="use_pushbullet" #if $sickbeard.USE_PUSHBULLET then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_pushbullet" id="use_pushbullet" #if $sickbeard.USE_PUSHBULLET then 'checked="checked"' else ''# />
<p>should SickGear send Pushbullet notifications ?</p>
</span>
</label>
@ -1320,7 +1320,7 @@
<label for="pushbullet_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="pushbullet_notify_onsnatch" id="pushbullet_notify_onsnatch" #if $sickbeard.PUSHBULLET_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushbullet_notify_onsnatch" id="pushbullet_notify_onsnatch" #if $sickbeard.PUSHBULLET_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -1329,7 +1329,7 @@
<label for="pushbullet_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="pushbullet_notify_ondownload" id="pushbullet_notify_ondownload" #if $sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushbullet_notify_ondownload" id="pushbullet_notify_ondownload" #if $sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -1338,7 +1338,7 @@
<label for="pushbullet_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="pushbullet_notify_onsubtitledownload" id="pushbullet_notify_onsubtitledownload" #if $sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="pushbullet_notify_onsubtitledownload" id="pushbullet_notify_onsubtitledownload" #if $sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -1388,7 +1388,7 @@
<label for="use_twitter">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_twitter" id="use_twitter" #if $sickbeard.USE_TWITTER then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_twitter" id="use_twitter" #if $sickbeard.USE_TWITTER then 'checked="checked"' else ''# />
<p>should SickGear post tweets on Twitter ?</p>
</span>
</label>
@ -1403,7 +1403,7 @@
<label for="twitter_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="twitter_notify_onsnatch" id="twitter_notify_onsnatch" #if $sickbeard.TWITTER_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="twitter_notify_onsnatch" id="twitter_notify_onsnatch" #if $sickbeard.TWITTER_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -1412,7 +1412,7 @@
<label for="twitter_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="twitter_notify_ondownload" id="twitter_notify_ondownload" #if $sickbeard.TWITTER_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="twitter_notify_ondownload" id="twitter_notify_ondownload" #if $sickbeard.TWITTER_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -1421,7 +1421,7 @@
<label for="twitter_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="twitter_notify_onsubtitledownload" id="twitter_notify_onsubtitledownload" #if $sickbeard.TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="twitter_notify_onsubtitledownload" id="twitter_notify_onsubtitledownload" #if $sickbeard.TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -1472,7 +1472,7 @@
<label for="use_trakt">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_trakt" id="use_trakt" #if $sickbeard.USE_TRAKT then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_trakt" id="use_trakt" #if $sickbeard.USE_TRAKT then 'checked="checked"' else ''# />
<p>should SickGear send Trakt.tv notifications ?</p>
</span>
</label>
@ -1515,7 +1515,7 @@
<span class="component-desc">
<select id="trakt_default_indexer" name="trakt_default_indexer" class="form-control input-sm">
#for $indexer in $sickbeard.indexerApi().indexers
<option value="$indexer" #if $indexer == $sickbeard.TRAKT_DEFAULT_INDEXER then "selected=\"selected\"" else ""#>$sickbeard.indexerApi().indexers[$indexer]</option>
<option value="$indexer" #if $indexer == $sickbeard.TRAKT_DEFAULT_INDEXER then 'selected="selected"' else ''#>$sickbeard.indexerApi().indexers[$indexer]</option>
#end for
</select>
</span>
@ -1525,7 +1525,7 @@
<label for="trakt_sync">
<span class="component-title">Sync libraries:</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="trakt_sync" id="trakt_sync" #if $sickbeard.TRAKT_SYNC then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="trakt_sync" id="trakt_sync" #if $sickbeard.TRAKT_SYNC then 'checked="checked"' else ''# />
<p>sync your SickGear show library with your trakt show library.</p>
</span>
</label>
@ -1534,7 +1534,7 @@
<label for="trakt_use_watchlist">
<span class="component-title">Use watchlist:</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="trakt_use_watchlist" id="trakt_use_watchlist" #if $sickbeard.TRAKT_USE_WATCHLIST then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="trakt_use_watchlist" id="trakt_use_watchlist" #if $sickbeard.TRAKT_USE_WATCHLIST then 'checked="checked"' else ''# />
<p>get new shows from your trakt watchlist.</p>
</span>
</label>
@ -1544,9 +1544,9 @@
<label for="trakt_method_add">
<span class="component-title">Watchlist add method:</span>
<select id="trakt_method_add" name="trakt_method_add" class="form-control input-sm">
<option value="0" #if $sickbeard.TRAKT_METHOD_ADD == 0 then "selected=\"selected\"" else ""#>Skip All</option>
<option value="1" #if $sickbeard.TRAKT_METHOD_ADD == 1 then "selected=\"selected\"" else ""#>Download Pilot Only</option>
<option value="2" #if $sickbeard.TRAKT_METHOD_ADD == 2 then "selected=\"selected\"" else ""#>Get whole show</option>
<option value="0" #if $sickbeard.TRAKT_METHOD_ADD == 0 then 'selected="selected"' else ''#>Skip All</option>
<option value="1" #if $sickbeard.TRAKT_METHOD_ADD == 1 then 'selected="selected"' else ''#>Download Pilot Only</option>
<option value="2" #if $sickbeard.TRAKT_METHOD_ADD == 2 then 'selected="selected"' else ''#>Get whole show</option>
</select>
</label>
<label>
@ -1558,7 +1558,7 @@
<label for="trakt_remove_watchlist">
<span class="component-title">Remove episode:</span>
<span class="component-desc">
<input type="checkbox" name="trakt_remove_watchlist" id="trakt_remove_watchlist" #if $sickbeard.TRAKT_REMOVE_WATCHLIST then "checked=\"checked\"" else ""# />
<input type="checkbox" name="trakt_remove_watchlist" id="trakt_remove_watchlist" #if $sickbeard.TRAKT_REMOVE_WATCHLIST then 'checked="checked"' else ''# />
<p>remove an episode from your watchlist after it is downloaded.</p>
</span>
</label>
@ -1567,7 +1567,7 @@
<label for="trakt_remove_serieslist">
<span class="component-title">Remove series:</span>
<span class="component-desc">
<input type="checkbox" name="trakt_remove_serieslist" id="trakt_remove_serieslist" #if $sickbeard.TRAKT_REMOVE_SERIESLIST then "checked=\"checked\"" else ""# />
<input type="checkbox" name="trakt_remove_serieslist" id="trakt_remove_serieslist" #if $sickbeard.TRAKT_REMOVE_SERIESLIST then 'checked="checked"' else ''# />
<p>remove the whole series from your watchlist after any download.</p>
</span>
</label>
@ -1576,7 +1576,7 @@
<label for="trakt_start_paused">
<span class="component-title">Start paused:</span>
<span class="component-desc">
<input type="checkbox" name="trakt_start_paused" id="trakt_start_paused" #if $sickbeard.TRAKT_START_PAUSED then "checked=\"checked\"" else ""# />
<input type="checkbox" name="trakt_start_paused" id="trakt_start_paused" #if $sickbeard.TRAKT_START_PAUSED then 'checked="checked"' else ''# />
<p>show's grabbed from your trakt watchlist start paused.</p>
</span>
</label>
@ -1600,7 +1600,7 @@
<label for="use_email">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_email" id="use_email" #if $sickbeard.USE_EMAIL then "checked=\"checked\"" else ""# />
<input type="checkbox" class="enabler" name="use_email" id="use_email" #if $sickbeard.USE_EMAIL then 'checked="checked"' else ''# />
<p>should SickGear send email notifications ?</p>
</span>
</label>
@ -1611,7 +1611,7 @@
<label for="email_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="email_notify_onsnatch" id="email_notify_onsnatch" #if $sickbeard.EMAIL_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<input type="checkbox" name="email_notify_onsnatch" id="email_notify_onsnatch" #if $sickbeard.EMAIL_NOTIFY_ONSNATCH then 'checked="checked"' else ''# />
<p>send a notification when a download starts ?</p>
</span>
</label>
@ -1620,7 +1620,7 @@
<label for="email_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="email_notify_ondownload" id="email_notify_ondownload" #if $sickbeard.EMAIL_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="email_notify_ondownload" id="email_notify_ondownload" #if $sickbeard.EMAIL_NOTIFY_ONDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
@ -1629,7 +1629,7 @@
<label for="email_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="email_notify_onsubtitledownload" id="email_notify_onsubtitledownload" #if $sickbeard.EMAIL_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<input type="checkbox" name="email_notify_onsubtitledownload" id="email_notify_onsubtitledownload" #if $sickbeard.EMAIL_NOTIFY_ONSUBTITLEDOWNLOAD then 'checked="checked"' else ''# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
@ -1668,7 +1668,7 @@
<label for="email_tls">
<span class="component-title">Use TLS</span>
<span class="component-desc">
<input type="checkbox" name="email_tls" id="email_tls" #if $sickbeard.EMAIL_TLS then "checked=\"checked\"" else ""# />
<input type="checkbox" name="email_tls" id="email_tls" #if $sickbeard.EMAIL_TLS then 'checked="checked"' else ''# />
<p>check to use TLS encryption.</p>
</span>
</label>

View file

@ -3,8 +3,8 @@
#from sickbeard.providers import thepiratebay
#from sickbeard.helpers import anon_url, starify
##
#set global $title="Config - Providers"
#set global $header="Search Providers"
#set global $title = 'Config - Providers'
#set global $header = 'Search Providers'
#set global $sbPath = '../..'
#set global $topmenu = 'config'
##
@ -39,7 +39,7 @@
#for $curNewznabProvider in $sickbeard.newznabProviderList:
\$(this).addProvider('$curNewznabProvider.getID()', '$curNewznabProvider.name', '$curNewznabProvider.url', '<%= starify(curNewznabProvider.key) %>', '$curNewznabProvider.catIDs', $int($curNewznabProvider.default), show_nzb_providers);
\$(this).addProvider('$curNewznabProvider.get_id()', '$curNewznabProvider.name', '$curNewznabProvider.url', '<%= starify(curNewznabProvider.key) %>', '$curNewznabProvider.cat_ids', $int($curNewznabProvider.default), show_nzb_providers);
#end for
@ -49,7 +49,7 @@
#for $curTorrentRssProvider in $sickbeard.torrentRssProviderList:
\$(this).addTorrentRssProvider('$curTorrentRssProvider.getID()', '$curTorrentRssProvider.name', '$curTorrentRssProvider.url', '<%= starify(curTorrentRssProvider.cookies) %>');
\$(this).addTorrentRssProvider('$curTorrentRssProvider.get_id()', '$curTorrentRssProvider.name', '$curTorrentRssProvider.url', '<%= starify(curTorrentRssProvider.cookies) %>');
#end for
@ -90,7 +90,7 @@
<p>At least one provider is required but two are recommended.</p>
#if $methods_notused
<blockquote style="margin: 20px 0"><%= '/'.join(x for x in methods_notused) %> providers can be enabled in <a href="$sbRoot/config/search/">Search Settings</a></blockquote>
<blockquote style="margin:20px 0"><%= '/'.join(x for x in methods_notused) %> providers can be enabled in <a href="$sbRoot/config/search/">Search Settings</a></blockquote>
#else
<br/>
#end if
@ -104,13 +104,12 @@
#elif $curProvider.providerType == $GenericProvider.TORRENT and not $sickbeard.USE_TORRENTS
#continue
#end if
#set $curName = $curProvider.getID()
#set $curName = $curProvider.get_id()
<li class="ui-state-default" id="$curName">
<input type="checkbox" id="enable_$curName" class="provider_enabler" <%= html_checked if curProvider.isEnabled() else '' %>/>
<a href="<%= anon_url(curProvider.url) %>" class="imgLink" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/providers/$curProvider.imageName()" alt="$curProvider.name" title="$curProvider.name" width="16" height="16" style="vertical-align:middle;"/></a>
<span style="vertical-align:middle;">$curProvider.name</span>
<input type="checkbox" id="enable_$curName" class="provider_enabler" <%= html_checked if curProvider.is_enabled() else '' %>/>
<a href="<%= anon_url(curProvider.url) %>" class="imgLink" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;"><img src="$sbRoot/images/providers/$curProvider.image_name()" alt="$curProvider.name" title="$curProvider.name" width="16" height="16" style="vertical-align:middle;"/></a>
<span style="vertical-align:middle">$curProvider.name</span>
<%= '*' if not curProvider.supportsBacklog else '' %>
<%= '**' if 'EZRSS' == curProvider.name else '' %>
<span class="ui-icon ui-icon-arrowthick-2-n-s pull-right" style="margin-top:3px"></span>
</li>
#end for
@ -125,7 +124,7 @@
##<h4 class="note">!</h4><p class="note">Provider is <b>NOT WORKING</b></p>
</div>
<input type="hidden" name="provider_order" id="provider_order" value="<%=" ".join([x.getID()+':'+str(int(x.isEnabled())) for x in sickbeard.providers.sortedProviderList()])%>"/>
<input type="hidden" name="provider_order" id="provider_order" value="<%=' '.join([x.get_id()+':'+str(int(x.is_enabled())) for x in sickbeard.providers.sortedProviderList()])%>"/>
<div style="width: 300px; float: right">
<div style="margin: 0px auto; width: 101px">
<input type="submit" class="btn config_submitter" value="Save Changes" />
@ -157,7 +156,7 @@
#elif $curProvider.providerType == $GenericProvider.TORRENT and not $sickbeard.USE_TORRENTS
#continue
#end if
#if $curProvider.isEnabled()
#if $curProvider.is_enabled()
$provider_config_list_enabled.append($curProvider)
#else
$provider_config_list.append($curProvider)
@ -169,14 +168,14 @@
#if $provider_config_list_enabled
<optgroup label="Enabled...">
#for $cur_provider in $provider_config_list_enabled:
<option value="$cur_provider.getID()">$cur_provider.name</option>
<option value="$cur_provider.get_id()">$cur_provider.name</option>
#end for
</optgroup>
#end if
#if $provider_config_list
<optgroup label="Not Enabled...">
#for $cur_provider in $provider_config_list
<option value="$cur_provider.getID()">$cur_provider.name</option>
<option value="$cur_provider.get_id()">$cur_provider.name</option>
#end for
</optgroup>
#end if
@ -188,76 +187,71 @@
</label>
</div>
<!-- start div for editing providers //-->
#for $curNewznabProvider in [$curProvider for $curProvider in $sickbeard.newznabProviderList]
<div class="providerDiv" id="${curNewznabProvider.getID()}Div">
<div class="providerDiv" id="${curNewznabProvider.get_id()}Div">
#if $curNewznabProvider.default and $curNewznabProvider.needs_auth
<div class="field-pair">
<label for="${curNewznabProvider.getID()}_url">
<label for="${curNewznabProvider.get_id()}_url">
<span class="component-title">URL</span>
<span class="component-desc">
<input type="text" id="${curNewznabProvider.getID()}_url" value="$curNewznabProvider.url" class="form-control input-sm input350" disabled/>
<input type="text" id="${curNewznabProvider.get_id()}_url" value="$curNewznabProvider.url" class="form-control input-sm input350" disabled/>
</span>
</label>
</div>
<div class="field-pair">
<label for="${curNewznabProvider.getID()}_hash">
<label for="${curNewznabProvider.get_id()}_hash">
<span class="component-title">API key</span>
<span class="component-desc">
<input type="text" id="${curNewznabProvider.getID()}_hash" value="<%= starify(curNewznabProvider.key) %>" newznab_name="${curNewznabProvider.getID()}_hash" class="newznab_key form-control input-sm input350" />
<input type="text" id="${curNewznabProvider.get_id()}_hash" value="<%= starify(curNewznabProvider.key) %>" newznab_name="${curNewznabProvider.get_id()}_hash" class="newznab_key form-control input-sm input350" />
<div class="clear-left"><p>get API key from provider website</p></div>
</span>
</label>
</div>
#end if
#if $hasattr($curNewznabProvider, 'enable_recentsearch'):
#if $hasattr($curNewznabProvider, 'enable_recentsearch') and $curNewznabProvider.supportsBacklog:
<div class="field-pair">
<label for="${curNewznabProvider.getID()}_enable_recentsearch">
<label for="${curNewznabProvider.get_id()}_enable_recentsearch">
<span class="component-title">Enable recent searches</span>
<span class="component-desc">
<input type="checkbox" name="${curNewznabProvider.getID()}_enable_recentsearch" id="${curNewznabProvider.getID()}_enable_recentsearch" <%= html_checked if curNewznabProvider.enable_recentsearch else '' %>/>
<input type="checkbox" name="${curNewznabProvider.get_id()}_enable_recentsearch" id="${curNewznabProvider.get_id()}_enable_recentsearch" <%= html_checked if curNewznabProvider.enable_recentsearch else '' %>/>
<p>perform recent searches at provider</p>
</span>
</label>
</div>
#end if
#if $hasattr($curNewznabProvider, 'enable_backlog'):
#if $hasattr($curNewznabProvider, 'enable_backlog') and $curNewznabProvider.supportsBacklog:
<div class="field-pair">
<label for="${curNewznabProvider.getID()}_enable_backlog">
<label for="${curNewznabProvider.get_id()}_enable_backlog">
<span class="component-title">Enable backlog searches</span>
<span class="component-desc">
<input type="checkbox" name="${curNewznabProvider.getID()}_enable_backlog" id="${curNewznabProvider.getID()}_enable_backlog" <%= html_checked if curNewznabProvider.enable_backlog else '' %>/>
<input type="checkbox" name="${curNewznabProvider.get_id()}_enable_backlog" id="${curNewznabProvider.get_id()}_enable_backlog" <%= html_checked if curNewznabProvider.enable_backlog else '' %>/>
<p>perform backlog searches at provider</p>
</span>
</label>
</div>
#end if
#if $hasattr($curNewznabProvider, 'search_mode'):
#if $hasattr($curNewznabProvider, 'search_mode') and $curNewznabProvider.supportsBacklog:
<div class="field-pair">
<span class="component-title">Season search mode</span>
<span class="component-desc">
<label class="space-right">
<input type="radio" name="${curNewznabProvider.getID()}_search_mode" id="${curNewznabProvider.getID()}_search_mode_sponly" value="sponly" <%= html_checked if 'sponly' == curNewznabProvider.search_mode else '' %>/>season packs only
<input type="radio" name="${curNewznabProvider.get_id()}_search_mode" id="${curNewznabProvider.get_id()}_search_mode_sponly" value="sponly" <%= html_checked if 'sponly' == curNewznabProvider.search_mode else '' %>/>season packs only
</label>
<label>
<input type="radio" name="${curNewznabProvider.getID()}_search_mode" id="${curNewznabProvider.getID()}_search_mode_eponly" value="eponly" <%= html_checked if 'eponly' == curNewznabProvider.search_mode else '' %>/>episodes only
<input type="radio" name="${curNewznabProvider.get_id()}_search_mode" id="${curNewznabProvider.get_id()}_search_mode_eponly" value="eponly" <%= html_checked if 'eponly' == curNewznabProvider.search_mode else '' %>/>episodes only
</label>
<p>when searching for complete seasons, search for packs or collect single episodes</p>
</span>
</div>
#end if
#if $hasattr($curNewznabProvider, 'search_fallback'):
#if $hasattr($curNewznabProvider, 'search_fallback') and $curNewznabProvider.supportsBacklog:
<div class="field-pair">
<label for="${curNewznabProvider.getID()}_search_fallback">
<label for="${curNewznabProvider.get_id()}_search_fallback">
<span class="component-title">Season search fallback</span>
<span class="component-desc">
<input type="checkbox" name="${curNewznabProvider.getID()}_search_fallback" id="${curNewznabProvider.getID()}_search_fallback" <%= html_checked if curNewznabProvider.search_fallback else '' %>/>
<input type="checkbox" name="${curNewznabProvider.get_id()}_search_fallback" id="${curNewznabProvider.get_id()}_search_fallback" <%= html_checked if curNewznabProvider.search_fallback else '' %>/>
<p>run the alternate season search mode when a complete season is not found</p>
</span>
</label>
@ -265,218 +259,205 @@
#end if
</div>
#end for
##
##
#for $curNzbProvider in [$curProvider for $curProvider in $sickbeard.providers.sortedProviderList() if $curProvider.providerType == $GenericProvider.NZB and $curProvider not in $sickbeard.newznabProviderList]:
<div class="providerDiv" id="${curNzbProvider.getID()}Div">
<div class="providerDiv" id="${curNzbProvider.get_id()}Div">
#if $hasattr($curNzbProvider, 'username'):
<div class="field-pair">
<label for="${curNzbProvider.getID()}_username">
<label for="${curNzbProvider.get_id()}_username">
<span class="component-title">Username</span>
<span class="component-desc">
<input type="text" name="${curNzbProvider.getID()}_username" value="$curNzbProvider.username" class="form-control input-sm input350" />
<input type="text" name="${curNzbProvider.get_id()}_username" value="$curNzbProvider.username" class="form-control input-sm input350" />
</span>
</label>
</div>
#end if
#if $hasattr($curNzbProvider, 'api_key'):
<div class="field-pair">
<label for="${curNzbProvider.getID()}_api_key">
<label for="${curNzbProvider.get_id()}_api_key">
<span class="component-title">API key</span>
<span class="component-desc">
<input type="text" name="${curNzbProvider.getID()}_api_key" value="<%= starify(curNzbProvider.api_key) %>" class="form-control input-sm input350" />
#set $field_name = curNzbProvider.get_id() + '_api_key'
<input type="text" name="$field_name" value="<%= starify(curNzbProvider.api_key) %>" class="form-control input-sm input350" />
#if callable(getattr(curNzbProvider, 'ui_string'))
<div class="clear-left"><p>${curNzbProvider.ui_string($field_name)}</p></div>
#end if
</span>
</label>
</div>
#end if
#if $hasattr($curNzbProvider, 'enable_recentsearch'):
#if $hasattr($curNzbProvider, 'enable_recentsearch') and $curNzbProvider.supportsBacklog:
<div class="field-pair">
<label for="${curNzbProvider.getID()}_enable_recentsearch">
<label for="${curNzbProvider.get_id()}_enable_recentsearch">
<span class="component-title">Enable recent searches</span>
<span class="component-desc">
<input type="checkbox" name="${curNzbProvider.getID()}_enable_recentsearch" id="${curNzbProvider.getID()}_enable_recentsearch" <%= html_checked if curNzbProvider.enable_recentsearch else '' %>/>
<input type="checkbox" name="${curNzbProvider.get_id()}_enable_recentsearch" id="${curNzbProvider.get_id()}_enable_recentsearch" <%= html_checked if curNzbProvider.enable_recentsearch else '' %>/>
<p>enable provider to perform recent searches.</p>
</span>
</label>
</div>
#end if
#if $hasattr($curNzbProvider, 'enable_backlog'):
#if $hasattr($curNzbProvider, 'enable_backlog') and $curNzbProvider.supportsBacklog:
<div class="field-pair">
<label for="${curNzbProvider.getID()}_enable_backlog">
<label for="${curNzbProvider.get_id()}_enable_backlog">
<span class="component-title">Enable backlog searches</span>
<span class="component-desc">
<input type="checkbox" name="${curNzbProvider.getID()}_enable_backlog" id="${curNzbProvider.getID()}_enable_backlog" <%= html_checked if curNzbProvider.enable_backlog else '' %>/>
<input type="checkbox" name="${curNzbProvider.get_id()}_enable_backlog" id="${curNzbProvider.get_id()}_enable_backlog" <%= html_checked if curNzbProvider.enable_backlog else '' %>/>
<p>enable provider to perform backlog searches.</p>
</span>
</label>
</div>
#end if
#if $hasattr($curNzbProvider, 'search_fallback'):
#if $hasattr($curNzbProvider, 'search_mode') and $curNzbProvider.supportsBacklog:
<div class="field-pair">
<label for="${curNzbProvider.getID()}_search_fallback">
<span class="component-title">Season search mode</span>
<span class="component-desc">
<label class="space-right">
<input type="radio" name="${curNzbProvider.get_id()}_search_mode" id="${curNzbProvider.get_id()}_search_mode_sponly" value="sponly" <%= html_checked if 'sponly' == curNzbProvider.search_mode else '' %>/>season packs only
</label>
<label>
<input type="radio" name="${curNzbProvider.get_id()}_search_mode" id="${curNzbProvider.get_id()}_search_mode_eponly" value="eponly" <%= html_checked if 'eponly' == curNzbProvider.search_mode else '' %>/>episodes only
</label>
<p>when searching for complete seasons, search for packs or collect single episodes</p>
</span>
</div>
#end if
#if $hasattr($curNzbProvider, 'search_fallback') and $curNzbProvider.supportsBacklog:
<div class="field-pair">
<label for="${curNzbProvider.get_id()}_search_fallback">
<span class="component-title">Season search fallback</span>
<span class="component-desc">
<input type="checkbox" name="${curNzbProvider.getID()}_search_fallback" id="${curNzbProvider.getID()}_search_fallback" <%= html_checked if curNzbProvider.search_fallback else '' %>/>
<p>when searching for a complete season depending on search mode you may return no results, this helps by restarting the search using the opposite search mode.</p>
<input type="checkbox" name="${curNzbProvider.get_id()}_search_fallback" id="${curNzbProvider.get_id()}_search_fallback" <%= html_checked if curNzbProvider.search_fallback else '' %>/>
<p>run the alternate season search mode when a complete season is not found</p>
</span>
</label>
</div>
#end if
#if $hasattr($curNzbProvider, 'search_mode'):
#if not $curNzbProvider.supportsBacklog:
<div class="field-pair">
<label>
<span class="component-title">Season search mode</span>
<span class="component-desc">
<p>when searching for complete seasons you can choose to have it look for season packs only, or choose to have it build a complete season from just single episodes.</p>
</span>
</label>
<label>
<span class="component-title"></span>
<span class="component-desc">
<input type="radio" name="${curNzbProvider.getID()}_search_mode" id="${curNzbProvider.getID()}_search_mode_sponly" value="sponly" <%= html_checked if 'sponly' == curNzbProvider.search_mode else '' %>/>season packs only.
</span>
</label>
<label>
<span class="component-title"></span>
<span class="component-desc">
<input type="radio" name="${curNzbProvider.getID()}_search_mode" id="${curNzbProvider.getID()}_search_mode_eponly" value="eponly" <%= html_checked if 'eponly' == curNzbProvider.search_mode else '' %>/>episodes only.
</span>
</label>
<span class="component-desc">The latest releases are the focus of this provider, no backlog searching</span>
</div>
#end if
</div>
#end for
##
##
#for $curTorrentProvider in [$curProvider for $curProvider in $sickbeard.providers.sortedProviderList() if $curProvider.providerType == $GenericProvider.TORRENT]:
<div class="providerDiv" id="${curTorrentProvider.getID()}Div">
<div class="providerDiv" id="${curTorrentProvider.get_id()}Div">
#if $hasattr($curTorrentProvider, 'api_key'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_api_key">
<label for="${curTorrentProvider.get_id()}_api_key">
<span class="component-title">Api key:</span>
<span class="component-desc">
<input type="text" name="${curTorrentProvider.getID()}_api_key" id="${curTorrentProvider.getID()}_api_key" value="<%= starify(curTorrentProvider.api_key) %>" class="form-control input-sm input350" />
<input type="text" name="${curTorrentProvider.get_id()}_api_key" id="${curTorrentProvider.get_id()}_api_key" value="<%= starify(curTorrentProvider.api_key) %>" class="form-control input-sm input350" />
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'digest'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_digest">
<label for="${curTorrentProvider.get_id()}_digest">
<span class="component-title">Digest:</span>
<span class="component-desc">
<input type="text" name="${curTorrentProvider.getID()}_digest" id="${curTorrentProvider.getID()}_digest" value="$curTorrentProvider.digest" class="form-control input-sm input350" />
<input type="text" name="${curTorrentProvider.get_id()}_digest" id="${curTorrentProvider.get_id()}_digest" value="$curTorrentProvider.digest" class="form-control input-sm input350" />
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'hash'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_hash">
<label for="${curTorrentProvider.get_id()}_hash">
<span class="component-title">Hash:</span>
<span class="component-desc">
<input type="text" name="${curTorrentProvider.getID()}_hash" id="${curTorrentProvider.getID()}_hash" value="$curTorrentProvider.hash" class="form-control input-sm input350" />
<input type="text" name="${curTorrentProvider.get_id()}_hash" id="${curTorrentProvider.get_id()}_hash" value="$curTorrentProvider.hash" class="form-control input-sm input350" />
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'username'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_username">
<label for="${curTorrentProvider.get_id()}_username">
<span class="component-title">Username:</span>
<span class="component-desc">
<input type="text" name="${curTorrentProvider.getID()}_username" id="${curTorrentProvider.getID()}_username" value="$curTorrentProvider.username" class="form-control input-sm input350" />
<input type="text" name="${curTorrentProvider.get_id()}_username" id="${curTorrentProvider.get_id()}_username" value="$curTorrentProvider.username" class="form-control input-sm input350" />
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'password'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_password">
<label for="${curTorrentProvider.get_id()}_password">
<span class="component-title">Password:</span>
<span class="component-desc">
<input type="password" name="${curTorrentProvider.getID()}_password" id="${curTorrentProvider.getID()}_password" value="#echo '*' * len($curTorrentProvider.password)#" class="form-control input-sm input350" />
<input type="password" name="${curTorrentProvider.get_id()}_password" id="${curTorrentProvider.get_id()}_password" value="#echo '*' * len($curTorrentProvider.password)#" class="form-control input-sm input350" />
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'passkey'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_passkey">
<label for="${curTorrentProvider.get_id()}_passkey">
<span class="component-title">Passkey:</span>
<span class="component-desc">
<input type="text" name="${curTorrentProvider.getID()}_passkey" id="${curTorrentProvider.getID()}_passkey" value="<%= starify(curTorrentProvider.passkey) %>" class="form-control input-sm input350" />
<input type="text" name="${curTorrentProvider.get_id()}_passkey" id="${curTorrentProvider.get_id()}_passkey" value="<%= starify(curTorrentProvider.passkey) %>" class="form-control input-sm input350" />
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'ratio'):
#if $hasattr($curTorrentProvider, '_seed_ratio') and 'blackhole' != $sickbeard.TORRENT_METHOD:
#set $torrent_method_text = {'blackhole': 'Black hole', 'utorrent': 'uTorrent', 'transmission': 'Transmission', 'deluge': 'Deluge', 'download_station': 'Synology DS', 'rtorrent': 'rTorrent'}
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_ratio">
<span class="component-title" id="${curTorrentProvider.getID()}_ratio_desc">Seed ratio:</span>
<label for="${curTorrentProvider.get_id()}_ratio">
<span class="component-title" id="${curTorrentProvider.get_id()}_ratio_desc">Seed until ratio (the goal)</span>
<span class="component-desc">
<input type="number" step="0.1" name="${curTorrentProvider.getID()}_ratio" id="${curTorrentProvider.getID()}_ratio" value="$curTorrentProvider.ratio" class="form-control input-sm input75" />
</span>
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">
<p>stop transfer when ratio is reached<br>(-1 SickGear default to seed forever, or leave blank for downloader default)</p>
<input type="number" step="0.1" name="${curTorrentProvider.get_id()}_ratio" id="${curTorrentProvider.get_id()}_ratio" value="$curTorrentProvider._seed_ratio" class="form-control input-sm input75" />
<p>this ratio is requested of each download sent to $torrent_method_text[$sickbeard.TORRENT_METHOD]</p>
<div class="clear-left"><p>(set -1 to seed forever, or leave blank for the $torrent_method_text[$sickbeard.TORRENT_METHOD] default)</p></div>
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'minseed'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_minseed">
<span class="component-title" id="${curTorrentProvider.getID()}_minseed_desc">Minimum seeders:</span>
<label for="${curTorrentProvider.get_id()}_minseed">
<span class="component-title" id="${curTorrentProvider.get_id()}_minseed_desc">Minimum seeders</span>
<span class="component-desc">
<input type="number" name="${curTorrentProvider.getID()}_minseed" id="${curTorrentProvider.getID()}_minseed" value="$curTorrentProvider.minseed" class="form-control input-sm input75" />
<input type="number" name="${curTorrentProvider.get_id()}_minseed" id="${curTorrentProvider.get_id()}_minseed" value="$curTorrentProvider.minseed" class="form-control input-sm input75" />
<p>a release must have to be snatch worthy</p>
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'minleech'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_minleech">
<span class="component-title" id="${curTorrentProvider.getID()}_minleech_desc">Minimum leechers:</span>
<label for="${curTorrentProvider.get_id()}_minleech">
<span class="component-title" id="${curTorrentProvider.get_id()}_minleech_desc">Minimum leechers</span>
<span class="component-desc">
<input type="number" name="${curTorrentProvider.getID()}_minleech" id="${curTorrentProvider.getID()}_minleech" value="$curTorrentProvider.minleech" class="form-control input-sm input75" />
<input type="number" name="${curTorrentProvider.get_id()}_minleech" id="${curTorrentProvider.get_id()}_minleech" value="$curTorrentProvider.minleech" class="form-control input-sm input75" />
<p>a release must have to be snatch worthy</p>
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'proxy'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_proxy">
<label for="${curTorrentProvider.get_id()}_proxy">
<span class="component-title">Access provider via proxy</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="${curTorrentProvider.getID()}_proxy" id="${curTorrentProvider.getID()}_proxy" <%= html_checked if curTorrentProvider.proxy.enabled else '' %>/>
<input type="checkbox" class="enabler" name="${curTorrentProvider.get_id()}_proxy" id="${curTorrentProvider.get_id()}_proxy" <%= html_checked if curTorrentProvider.proxy.enabled else '' %>/>
<p>to bypass country blocking mechanisms</p>
</span>
</label>
</div>
#if $hasattr($curTorrentProvider.proxy, 'url'):
<div class="field-pair content_${curTorrentProvider.getID()}_proxy" id="content_${curTorrentProvider.getID()}_proxy">
<label for="${curTorrentProvider.getID()}_proxy_url">
<div class="field-pair content_${curTorrentProvider.get_id()}_proxy" id="content_${curTorrentProvider.get_id()}_proxy">
<label for="${curTorrentProvider.get_id()}_proxy_url">
<span class="component-title">Proxy URL:</span>
<span class="component-desc">
<select name="${curTorrentProvider.getID()}_proxy_url" id="${curTorrentProvider.getID()}_proxy_url" class="form-control input-sm">
<select name="${curTorrentProvider.get_id()}_proxy_url" id="${curTorrentProvider.get_id()}_proxy_url" class="form-control input-sm">
#for $i in $curTorrentProvider.proxy.urls.keys():
<option value="$curTorrentProvider.proxy.urls[$i]" <%= html_selected if curTorrentProvider.proxy.url == curTorrentProvider.proxy.urls[i] else '' %>>$i</option>
#end for
@ -486,85 +467,71 @@
</div>
#end if
#end if
#if $hasattr($curTorrentProvider, 'confirmed'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_confirmed">
<label for="${curTorrentProvider.get_id()}_confirmed">
<span class="component-title">Confirmed download</span>
<span class="component-desc">
<input type="checkbox" name="${curTorrentProvider.getID()}_confirmed" id="${curTorrentProvider.getID()}_confirmed" <%= html_checked if curTorrentProvider.confirmed else '' %>/>
<input type="checkbox" name="${curTorrentProvider.get_id()}_confirmed" id="${curTorrentProvider.get_id()}_confirmed" <%= html_checked if curTorrentProvider.confirmed else '' %>/>
<p>only download torrents from trusted or verified uploaders ?</p>
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'freeleech'):
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_freeleech">
<label for="${curTorrentProvider.get_id()}_freeleech">
<span class="component-title">Freeleech</span>
<span class="component-desc">
<input type="checkbox" name="${curTorrentProvider.getID()}_freeleech" id="${curTorrentProvider.getID()}_freeleech" <%= html_checked if curTorrentProvider.freeleech else '' %>/>
<input type="checkbox" name="${curTorrentProvider.get_id()}_freeleech" id="${curTorrentProvider.get_id()}_freeleech" <%= html_checked if curTorrentProvider.freeleech else '' %>/>
<p>only download <b>[FreeLeech]</b> torrents.</p>
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'enable_recentsearch'):
#if $hasattr($curTorrentProvider, 'enable_recentsearch') and $curTorrentProvider.supportsBacklog:
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_enable_recentsearch">
<label for="${curTorrentProvider.get_id()}_enable_recentsearch">
<span class="component-title">Enable recent searches</span>
<span class="component-desc">
<input type="checkbox" name="${curTorrentProvider.getID()}_enable_recentsearch" id="${curTorrentProvider.getID()}_enable_recentsearch" <%= html_checked if curTorrentProvider.enable_recentsearch else '' %>/>
<input type="checkbox" name="${curTorrentProvider.get_id()}_enable_recentsearch" id="${curTorrentProvider.get_id()}_enable_recentsearch" <%= html_checked if curTorrentProvider.enable_recentsearch else '' %>/>
<p>enable provider to perform recent searches.</p>
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'enable_backlog'):
#if $hasattr($curTorrentProvider, 'enable_backlog') and $curTorrentProvider.supportsBacklog:
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_enable_backlog">
<label for="${curTorrentProvider.get_id()}_enable_backlog">
<span class="component-title">Enable backlog searches</span>
<span class="component-desc">
<input type="checkbox" name="${curTorrentProvider.getID()}_enable_backlog" id="${curTorrentProvider.getID()}_enable_backlog" <%= html_checked if curTorrentProvider.enable_backlog else '' %>/>
<input type="checkbox" name="${curTorrentProvider.get_id()}_enable_backlog" id="${curTorrentProvider.get_id()}_enable_backlog" <%= html_checked if curTorrentProvider.enable_backlog else '' %>/>
<p>enable provider to perform backlog searches.</p>
</span>
</label>
</div>
#end if
#if $hasattr($curTorrentProvider, 'search_fallback'):
#if $hasattr($curTorrentProvider, 'search_mode') and $curTorrentProvider.supportsBacklog:
<div class="field-pair">
<label for="${curTorrentProvider.getID()}_search_fallback">
<span class="component-title">Season search fallback</span>
<span class="component-desc">
<input type="checkbox" name="${curTorrentProvider.getID()}_search_fallback" id="${curTorrentProvider.getID()}_search_fallback" <%= html_checked if curTorrentProvider.search_fallback else '' %>/>
<p>when searching for a complete season depending on search mode you may return no results, this helps by restarting the search using the opposite search mode.</p>
</span>
</label>
<span class="component-title">Season search mode</span>
<span class="component-desc">
<label class="space-right">
<input type="radio" name="${curTorrentProvider.get_id()}_search_mode" id="${curTorrentProvider.get_id()}_search_mode_sponly" value="sponly" <%= html_checked if 'sponly' == curTorrentProvider.search_mode else '' %>/>season packs only
</label>
<label>
<input type="radio" name="${curTorrentProvider.get_id()}_search_mode" id="${curTorrentProvider.get_id()}_search_mode_eponly" value="eponly" <%= html_checked if 'eponly' == curTorrentProvider.search_mode else '' %>/>episodes only
</label>
<p>when searching for complete seasons, search for packs or collect single episodes</p>
</span>
</div>
#end if
#if $hasattr($curTorrentProvider, 'search_mode'):
#if $hasattr($curTorrentProvider, 'search_fallback') and $curTorrentProvider.supportsBacklog:
<div class="field-pair">
<label>
<span class="component-title">Season search mode</span>
<label for="${curTorrentProvider.get_id()}_search_fallback">
<span class="component-title">Season search fallback</span>
<span class="component-desc">
<p>when searching for complete seasons you can choose to have it look for season packs only, or choose to have it build a complete season from just single episodes.</p>
</span>
</label>
<label>
<span class="component-title"></span>
<span class="component-desc">
<input type="radio" name="${curTorrentProvider.getID()}_search_mode" id="${curTorrentProvider.getID()}_search_mode_sponly" value="sponly" <%= html_checked if 'sponly' == curTorrentProvider.search_mode else '' %>/>season packs only.
</span>
</label>
<label>
<span class="component-title"></span>
<span class="component-desc">
<input type="radio" name="${curTorrentProvider.getID()}_search_mode" id="${curTorrentProvider.getID()}_search_mode_eponly" value="eponly" <%= html_checked if 'eponly' == curTorrentProvider.search_mode else '' %>/>episodes only.
<input type="checkbox" name="${curTorrentProvider.get_id()}_search_fallback" id="${curTorrentProvider.get_id()}_search_fallback" <%= html_checked if curTorrentProvider.search_fallback else '' %>/>
<p>run the alternate season search mode when a complete season is not found</p>
</span>
</label>
</div>

View file

@ -82,7 +82,7 @@
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_qualityChooser.tmpl')
#if $anyQualities + $bestQualities
<div class="field-pair">
<div class="field-pair show-if-quality-custom">
<label for="archive_firstmatch">
<span class="component-title">End upgrade on first match</span>
<span class="component-desc">
@ -211,6 +211,20 @@
</label>
</div>
<div class="field-pair#if $sickbeard.SHOWLIST_TAGVIEW != 'custom' then ' hidden' else ''#" style="margin-bottom:10px">
<label for="tag">
<span class="component-title">Show is in group</span>
<span class="component-desc">
<select name="tag" id="tag" class="form-control form-control-inline input-sm">
#for $tag in $sickbeard.SHOW_TAGS:
<option value="$tag" #if $tag == $show.tag then 'selected="selected"' else ''#>$tag#echo ('', ' (default)')['Show List' == $tag]#</option>
#end for
</select>
<span>and is displayed on the show list page under this section</span>
</span>
</label>
</div>
<div class="field-pair">
<label for="sports">
<span class="component-title">Show is sports</span>
@ -231,20 +245,6 @@
</label>
</div>
<div class="field-pair#if $sickbeard.SHOWLIST_TAGVIEW != 'custom' then ' hidden' else ''#" style="margin-bottom:10px">
<label for="tag">
<span class="component-title">Show is grouped in</span>
<span class="component-desc">
<select name="tag" id="tag" class="form-control form-control-inline input-sm">
#for $tag in $sickbeard.SHOW_TAGS:
<option value="$tag" #if $tag == $show.tag then 'selected="selected"' else ''#>$tag#echo ('', ' (default)')['Show List' == $tag]#</option>
#end for
</select>
<span>and displays on the show list page under this section</span>
</span>
</label>
</div>
#if $show.is_anime:
#import sickbeard.blackandwhitelist
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_blackwhitelist.tmpl')

View file

@ -139,9 +139,9 @@
#else
#if 0 < $hItem['provider']
#if $curStatus in [SNATCHED, FAILED]
#set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($hItem['provider']))
#set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($hItem['provider']))
#if None is not $provider
<img src="$sbRoot/images/providers/<%= provider.imageName() %>" width="16" height="16" /><span>$provider.name</span>
<img src="$sbRoot/images/providers/<%= provider.image_name() %>" width="16" height="16" /><span>$provider.name</span>
#else
<img src="$sbRoot/images/providers/missing.png" width="16" height="16" title="missing provider" /><span>Missing Provider</span>
#end if
@ -186,10 +186,10 @@
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action['action']))
#set $basename = $os.path.basename($action['resource'])
#if $curStatus in [SNATCHED, FAILED]
#set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($action['provider']))
#set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($action['provider']))
#if None is not $provider
#set $prov_list += ['<span%s><img class="help" src="%s/images/providers/%s" width="16" height="16" alt="%s" title="%s.. %s: %s" /></span>'\
% (('', ' class="fail"')[FAILED == $curStatus], $sbRoot, $provider.imageName(), $provider.name,
% (('', ' class="fail"')[FAILED == $curStatus], $sbRoot, $provider.image_name(), $provider.name,
('%s%s' % ($order, 'th' if $order in [11, 12, 13] or str($order)[-1] not in $ordinal_indicators else $ordinal_indicators[str($order)[-1]]), 'Snatch failed')[FAILED == $curStatus],
$provider.name, $basename)]
#set $order += (0, 1)[SNATCHED == $curStatus]

View file

@ -19,7 +19,7 @@
</div>
<div id="customQualityWrapper">
<div id="customQuality">
<div id="customQuality" class="show-if-quality-custom">
<div class="component-group-desc">
<p>An <em>Initial</em> quality episode must be found before an <em>Upgrade to</em> selection is considered.</p>
</div>

View file

@ -72,9 +72,9 @@
<tr>
<td class="text-nowrap text-left">#echo re.sub('"', '', $hItem['release'])#</td>
<td>#echo ($hItem['size'], '?')[-1 == $hItem['size']]#</td>
#set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($hItem['provider']))
#set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($hItem['provider']))
#if None is not $provider:
<td><img src="$sbRoot/images/providers/<%= provider.imageName() %>" width="16" height="16" alt="$provider.name" title="$provider.name" /></td>
<td><img src="$sbRoot/images/providers/<%= provider.image_name() %>" width="16" height="16" alt="$provider.name" title="$provider.name" /></td>
#else
<td><img src="$sbRoot/images/providers/missing.png" width="16" height="16" alt="missing provider" title="missing provider" /></td>
#end if

View file

@ -489,10 +489,11 @@ $(document).ready(function(){
$.get(sbRoot + '/home/getPushbulletDevices', {'accessToken': pushbullet_access_token})
.done(function (data) {
var devices = jQuery.parseJSON(data || '{}').devices;
var error = jQuery.parseJSON(data || '{}').error;
$('#pushbullet_device_list').html('');
if (devices) {
// add default option to send to all devices
$('#pushbullet_device_list').append('<option value="" selected="selected">-- All Devices --</option>');
if (devices) {
for (var i = 0; i < devices.length; i++) {
// only list active device targets
if (devices[i].active == true) {
@ -507,7 +508,11 @@ $(document).ready(function(){
}
$('#getPushbulletDevices').prop('disabled', false);
if (msg) {
$('#testPushbullet-result').html(msg);
if (error.message) {
$('#testPushbullet-result').html(error.message);
} else {
$('#testPushbullet-result').html(msg);
}
}
});

View file

@ -1,5 +1,5 @@
function setFromPresets (preset) {
var elCustomQuality = $('#customQuality'),
var elCustomQuality = $('.show-if-quality-custom'),
selected = 'selected';
if (0 == preset) {
elCustomQuality.show();

View file

@ -40,8 +40,8 @@ $(document).ready(function() {
if (is_default)
setDefault($('#rootDirs option').attr('id'));
$.get(sbRoot+'/config/general/saveRootDirs', { rootDirString: $('#rootDirText').val() });
refreshRootDirs();
$.get(sbRoot+'/config/general/saveRootDirs', { rootDirString: $('#rootDirText').val() });
}
function editRootDir(path) {

View file

@ -45,7 +45,7 @@ from .element import (
# The very first thing we do is give a useful error if someone is
# running this code under Python 3 without converting it.
syntax_error = u'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work. You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).'
'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work.'<>'You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).'
class BeautifulSoup(Tag):
"""
@ -77,6 +77,8 @@ class BeautifulSoup(Tag):
ASCII_SPACES = '\x20\x0a\x09\x0c\x0d'
NO_PARSER_SPECIFIED_WARNING = "No parser was explicitly specified, so I'm using the best available parser for this system (\"%(parser)s\"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.\n\nTo get rid of this warning, change this:\n\n BeautifulSoup([your markup])\n\nto this:\n\n BeautifulSoup([your markup], \"%(parser)s\")\n"
def __init__(self, markup="", features=None, builder=None,
parse_only=None, from_encoding=None, **kwargs):
"""The Soup object is initialized as the 'root tag', and the
@ -114,9 +116,9 @@ class BeautifulSoup(Tag):
del kwargs['isHTML']
warnings.warn(
"BS4 does not respect the isHTML argument to the "
"BeautifulSoup constructor. You can pass in features='html' "
"or features='xml' to get a builder capable of handling "
"one or the other.")
"BeautifulSoup constructor. Suggest you use "
"features='lxml' for HTML and features='lxml-xml' for "
"XML.")
def deprecated_argument(old_name, new_name):
if old_name in kwargs:
@ -140,6 +142,7 @@ class BeautifulSoup(Tag):
"__init__() got an unexpected keyword argument '%s'" % arg)
if builder is None:
original_features = features
if isinstance(features, basestring):
features = [features]
if features is None or len(features) == 0:
@ -151,6 +154,11 @@ class BeautifulSoup(Tag):
"requested: %s. Do you need to install a parser library?"
% ",".join(features))
builder = builder_class()
if not (original_features == builder.NAME or
original_features in builder.ALTERNATE_NAMES):
warnings.warn(self.NO_PARSER_SPECIFIED_WARNING % dict(
parser=builder.NAME))
self.builder = builder
self.is_xml = builder.is_xml
self.builder.soup = self
@ -178,6 +186,8 @@ class BeautifulSoup(Tag):
# system. Just let it go.
pass
if is_file:
if isinstance(markup, unicode):
markup = markup.encode("utf8")
warnings.warn(
'"%s" looks like a filename, not markup. You should probably open this file and pass the filehandle into Beautiful Soup.' % markup)
if markup[:5] == "http:" or markup[:6] == "https:":
@ -185,6 +195,8 @@ class BeautifulSoup(Tag):
# Python 3 otherwise.
if ((isinstance(markup, bytes) and not b' ' in markup)
or (isinstance(markup, unicode) and not u' ' in markup)):
if isinstance(markup, unicode):
markup = markup.encode("utf8")
warnings.warn(
'"%s" looks like a URL. Beautiful Soup is not an HTTP client. You should probably use an HTTP client to get the document behind the URL, and feed that document to Beautiful Soup.' % markup)

View file

@ -80,6 +80,8 @@ builder_registry = TreeBuilderRegistry()
class TreeBuilder(object):
"""Turn a document into a Beautiful Soup object tree."""
NAME = "[Unknown tree builder]"
ALTERNATE_NAMES = []
features = []
is_xml = False

View file

@ -22,7 +22,9 @@ from bs4.element import (
class HTML5TreeBuilder(HTMLTreeBuilder):
"""Use html5lib to build a tree."""
features = ['html5lib', PERMISSIVE, HTML_5, HTML]
NAME = "html5lib"
features = [NAME, PERMISSIVE, HTML_5, HTML]
def prepare_markup(self, markup, user_specified_encoding):
# Store the user-specified encoding for use later on.
@ -161,6 +163,12 @@ class Element(html5lib.treebuilders._base.Node):
# immediately after the parent, if it has no children.)
if self.element.contents:
most_recent_element = self.element._last_descendant(False)
elif self.element.next_element is not None:
# Something from further ahead in the parse tree is
# being inserted into this earlier element. This is
# very annoying because it means an expensive search
# for the last element in the tree.
most_recent_element = self.soup._last_descendant()
else:
most_recent_element = self.element

View file

@ -19,10 +19,8 @@ import warnings
# At the end of this file, we monkeypatch HTMLParser so that
# strict=True works well on Python 3.2.2.
major, minor, release = sys.version_info[:3]
CONSTRUCTOR_TAKES_STRICT = (
major > 3
or (major == 3 and minor > 2)
or (major == 3 and minor == 2 and release >= 3))
CONSTRUCTOR_TAKES_STRICT = major == 3 and minor == 2 and release >= 3
CONSTRUCTOR_TAKES_CONVERT_CHARREFS = major == 3 and minor >= 4
from bs4.element import (
CData,
@ -63,7 +61,8 @@ class BeautifulSoupHTMLParser(HTMLParser):
def handle_charref(self, name):
# XXX workaround for a bug in HTMLParser. Remove this once
# it's fixed.
# it's fixed in all supported versions.
# http://bugs.python.org/issue13633
if name.startswith('x'):
real_name = int(name.lstrip('x'), 16)
elif name.startswith('X'):
@ -113,14 +112,6 @@ class BeautifulSoupHTMLParser(HTMLParser):
def handle_pi(self, data):
self.soup.endData()
if data.endswith("?") and data.lower().startswith("xml"):
# "An XHTML processing instruction using the trailing '?'
# will cause the '?' to be included in data." - HTMLParser
# docs.
#
# Strip the question mark so we don't end up with two
# question marks.
data = data[:-1]
self.soup.handle_data(data)
self.soup.endData(ProcessingInstruction)
@ -128,11 +119,14 @@ class BeautifulSoupHTMLParser(HTMLParser):
class HTMLParserTreeBuilder(HTMLTreeBuilder):
is_xml = False
features = [HTML, STRICT, HTMLPARSER]
NAME = HTMLPARSER
features = [NAME, HTML, STRICT]
def __init__(self, *args, **kwargs):
if CONSTRUCTOR_TAKES_STRICT:
kwargs['strict'] = False
if CONSTRUCTOR_TAKES_CONVERT_CHARREFS:
kwargs['convert_charrefs'] = False
self.parser_args = (args, kwargs)
def prepare_markup(self, markup, user_specified_encoding=None,

View file

@ -7,7 +7,12 @@ from io import BytesIO
from StringIO import StringIO
import collections
from lxml import etree
from bs4.element import Comment, Doctype, NamespacedAttribute
from bs4.element import (
Comment,
Doctype,
NamespacedAttribute,
ProcessingInstruction,
)
from bs4.builder import (
FAST,
HTML,
@ -25,8 +30,10 @@ class LXMLTreeBuilderForXML(TreeBuilder):
is_xml = True
NAME = "lxml-xml"
# Well, it's permissive by XML parser standards.
features = [LXML, XML, FAST, PERMISSIVE]
features = [NAME, LXML, XML, FAST, PERMISSIVE]
CHUNK_SIZE = 512
@ -189,7 +196,9 @@ class LXMLTreeBuilderForXML(TreeBuilder):
self.nsmaps.pop()
def pi(self, target, data):
pass
self.soup.endData()
self.soup.handle_data(target + ' ' + data)
self.soup.endData(ProcessingInstruction)
def data(self, content):
self.soup.handle_data(content)
@ -212,7 +221,10 @@ class LXMLTreeBuilderForXML(TreeBuilder):
class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML):
features = [LXML, HTML, FAST, PERMISSIVE]
NAME = LXML
ALTERNATE_NAMES = ["lxml-html"]
features = ALTERNATE_NAMES + [NAME, HTML, FAST, PERMISSIVE]
is_xml = False
def default_parser(self, encoding):

View file

@ -548,17 +548,17 @@ class PageElement(object):
# Methods for supporting CSS selectors.
tag_name_re = re.compile('^[a-z0-9]+$')
tag_name_re = re.compile('^[a-zA-Z0-9][-.a-zA-Z0-9:_]*$')
# /^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
# \---/ \---/\-------------/ \-------/
# | | | |
# | | | The value
# | | ~,|,^,$,* or =
# | Attribute
# /^([a-zA-Z0-9][-.a-zA-Z0-9:_]*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
# \---------------------------/ \---/\-------------/ \-------/
# | | | |
# | | | The value
# | | ~,|,^,$,* or =
# | Attribute
# Tag
attribselect_re = re.compile(
r'^(?P<tag>\w+)?\[(?P<attribute>\w+)(?P<operator>[=~\|\^\$\*]?)' +
r'^(?P<tag>[a-zA-Z0-9][-.a-zA-Z0-9:_]*)?\[(?P<attribute>\w+)(?P<operator>[=~\|\^\$\*]?)' +
r'=?"?(?P<value>[^\]"]*)"?\]$'
)
@ -707,7 +707,7 @@ class CData(PreformattedString):
class ProcessingInstruction(PreformattedString):
PREFIX = u'<?'
SUFFIX = u'?>'
SUFFIX = u'>'
class Comment(PreformattedString):
@ -1203,192 +1203,206 @@ class Tag(PageElement):
_select_debug = False
def select(self, selector, _candidate_generator=None):
"""Perform a CSS selection operation on the current element."""
tokens = selector.split()
# Remove whitespace directly after the grouping operator ','
# then split into tokens.
tokens = re.sub(',[\s]*',',', selector).split()
current_context = [self]
if tokens[-1] in self._selector_combinators:
raise ValueError(
'Final combinator "%s" is missing an argument.' % tokens[-1])
if self._select_debug:
print 'Running CSS selector "%s"' % selector
for index, token in enumerate(tokens):
if self._select_debug:
print ' Considering token "%s"' % token
recursive_candidate_generator = None
tag_name = None
for index, token_group in enumerate(tokens):
new_context = []
new_context_ids = set([])
# Grouping selectors, ie: p,a
grouped_tokens = token_group.split(',')
if '' in grouped_tokens:
raise ValueError('Invalid group selection syntax: %s' % token_group)
if tokens[index-1] in self._selector_combinators:
# This token was consumed by the previous combinator. Skip it.
if self._select_debug:
print ' Token was consumed by the previous combinator.'
continue
# Each operation corresponds to a checker function, a rule
# for determining whether a candidate matches the
# selector. Candidates are generated by the active
# iterator.
checker = None
m = self.attribselect_re.match(token)
if m is not None:
# Attribute selector
tag_name, attribute, operator, value = m.groups()
checker = self._attribute_checker(operator, attribute, value)
for token in grouped_tokens:
if self._select_debug:
print ' Considering token "%s"' % token
recursive_candidate_generator = None
tag_name = None
elif '#' in token:
# ID selector
tag_name, tag_id = token.split('#', 1)
def id_matches(tag):
return tag.get('id', None) == tag_id
checker = id_matches
# Each operation corresponds to a checker function, a rule
# for determining whether a candidate matches the
# selector. Candidates are generated by the active
# iterator.
checker = None
elif '.' in token:
# Class selector
tag_name, klass = token.split('.', 1)
classes = set(klass.split('.'))
def classes_match(candidate):
return classes.issubset(candidate.get('class', []))
checker = classes_match
m = self.attribselect_re.match(token)
if m is not None:
# Attribute selector
tag_name, attribute, operator, value = m.groups()
checker = self._attribute_checker(operator, attribute, value)
elif ':' in token:
# Pseudo-class
tag_name, pseudo = token.split(':', 1)
if tag_name == '':
raise ValueError(
"A pseudo-class must be prefixed with a tag name.")
pseudo_attributes = re.match('([a-zA-Z\d-]+)\(([a-zA-Z\d]+)\)', pseudo)
found = []
if pseudo_attributes is not None:
pseudo_type, pseudo_value = pseudo_attributes.groups()
if pseudo_type == 'nth-of-type':
try:
pseudo_value = int(pseudo_value)
except:
elif '#' in token:
# ID selector
tag_name, tag_id = token.split('#', 1)
def id_matches(tag):
return tag.get('id', None) == tag_id
checker = id_matches
elif '.' in token:
# Class selector
tag_name, klass = token.split('.', 1)
classes = set(klass.split('.'))
def classes_match(candidate):
return classes.issubset(candidate.get('class', []))
checker = classes_match
elif ':' in token:
# Pseudo-class
tag_name, pseudo = token.split(':', 1)
if tag_name == '':
raise ValueError(
"A pseudo-class must be prefixed with a tag name.")
pseudo_attributes = re.match('([a-zA-Z\d-]+)\(([a-zA-Z\d]+)\)', pseudo)
found = []
if pseudo_attributes is not None:
pseudo_type, pseudo_value = pseudo_attributes.groups()
if pseudo_type == 'nth-of-type':
try:
pseudo_value = int(pseudo_value)
except:
raise NotImplementedError(
'Only numeric values are currently supported for the nth-of-type pseudo-class.')
if pseudo_value < 1:
raise ValueError(
'nth-of-type pseudo-class value must be at least 1.')
class Counter(object):
def __init__(self, destination):
self.count = 0
self.destination = destination
def nth_child_of_type(self, tag):
self.count += 1
if self.count == self.destination:
return True
if self.count > self.destination:
# Stop the generator that's sending us
# these things.
raise StopIteration()
return False
checker = Counter(pseudo_value).nth_child_of_type
else:
raise NotImplementedError(
'Only numeric values are currently supported for the nth-of-type pseudo-class.')
if pseudo_value < 1:
raise ValueError(
'nth-of-type pseudo-class value must be at least 1.')
class Counter(object):
def __init__(self, destination):
self.count = 0
self.destination = destination
'Only the following pseudo-classes are implemented: nth-of-type.')
def nth_child_of_type(self, tag):
self.count += 1
if self.count == self.destination:
return True
if self.count > self.destination:
# Stop the generator that's sending us
# these things.
raise StopIteration()
return False
checker = Counter(pseudo_value).nth_child_of_type
else:
raise NotImplementedError(
'Only the following pseudo-classes are implemented: nth-of-type.')
elif token == '*':
# Star selector -- matches everything
pass
elif token == '>':
# Run the next token as a CSS selector against the
# direct children of each tag in the current context.
recursive_candidate_generator = lambda tag: tag.children
elif token == '~':
# Run the next token as a CSS selector against the
# siblings of each tag in the current context.
recursive_candidate_generator = lambda tag: tag.next_siblings
elif token == '+':
# For each tag in the current context, run the next
# token as a CSS selector against the tag's next
# sibling that's a tag.
def next_tag_sibling(tag):
yield tag.find_next_sibling(True)
recursive_candidate_generator = next_tag_sibling
elif token == '*':
# Star selector -- matches everything
pass
elif token == '>':
# Run the next token as a CSS selector against the
# direct children of each tag in the current context.
recursive_candidate_generator = lambda tag: tag.children
elif token == '~':
# Run the next token as a CSS selector against the
# siblings of each tag in the current context.
recursive_candidate_generator = lambda tag: tag.next_siblings
elif token == '+':
# For each tag in the current context, run the next
# token as a CSS selector against the tag's next
# sibling that's a tag.
def next_tag_sibling(tag):
yield tag.find_next_sibling(True)
recursive_candidate_generator = next_tag_sibling
elif self.tag_name_re.match(token):
# Just a tag name.
tag_name = token
else:
raise ValueError(
'Unsupported or invalid CSS selector: "%s"' % token)
if recursive_candidate_generator:
# This happens when the selector looks like "> foo".
#
# The generator calls select() recursively on every
# member of the current context, passing in a different
# candidate generator and a different selector.
#
# In the case of "> foo", the candidate generator is
# one that yields a tag's direct children (">"), and
# the selector is "foo".
next_token = tokens[index+1]
def recursive_select(tag):
if self._select_debug:
print ' Calling select("%s") recursively on %s %s' % (next_token, tag.name, tag.attrs)
print '-' * 40
for i in tag.select(next_token, recursive_candidate_generator):
if self._select_debug:
print '(Recursive select picked up candidate %s %s)' % (i.name, i.attrs)
yield i
if self._select_debug:
print '-' * 40
_use_candidate_generator = recursive_select
elif _candidate_generator is None:
# By default, a tag's candidates are all of its
# children. If tag_name is defined, only yield tags
# with that name.
if self._select_debug:
if tag_name:
check = "[any]"
else:
check = tag_name
print ' Default candidate generator, tag name="%s"' % check
if self._select_debug:
# This is redundant with later code, but it stops
# a bunch of bogus tags from cluttering up the
# debug log.
def default_candidate_generator(tag):
for child in tag.descendants:
if not isinstance(child, Tag):
continue
if tag_name and not child.name == tag_name:
continue
yield child
_use_candidate_generator = default_candidate_generator
elif self.tag_name_re.match(token):
# Just a tag name.
tag_name = token
else:
_use_candidate_generator = lambda tag: tag.descendants
else:
_use_candidate_generator = _candidate_generator
new_context = []
new_context_ids = set([])
for tag in current_context:
if self._select_debug:
print " Running candidate generator on %s %s" % (
tag.name, repr(tag.attrs))
for candidate in _use_candidate_generator(tag):
if not isinstance(candidate, Tag):
continue
if tag_name and candidate.name != tag_name:
continue
if checker is not None:
try:
result = checker(candidate)
except StopIteration:
# The checker has decided we should no longer
# run the generator.
break
if checker is None or result:
raise ValueError(
'Unsupported or invalid CSS selector: "%s"' % token)
if recursive_candidate_generator:
# This happens when the selector looks like "> foo".
#
# The generator calls select() recursively on every
# member of the current context, passing in a different
# candidate generator and a different selector.
#
# In the case of "> foo", the candidate generator is
# one that yields a tag's direct children (">"), and
# the selector is "foo".
next_token = tokens[index+1]
def recursive_select(tag):
if self._select_debug:
print " SUCCESS %s %s" % (candidate.name, repr(candidate.attrs))
if id(candidate) not in new_context_ids:
# If a tag matches a selector more than once,
# don't include it in the context more than once.
new_context.append(candidate)
new_context_ids.add(id(candidate))
elif self._select_debug:
print " FAILURE %s %s" % (candidate.name, repr(candidate.attrs))
print ' Calling select("%s") recursively on %s %s' % (next_token, tag.name, tag.attrs)
print '-' * 40
for i in tag.select(next_token, recursive_candidate_generator):
if self._select_debug:
print '(Recursive select picked up candidate %s %s)' % (i.name, i.attrs)
yield i
if self._select_debug:
print '-' * 40
_use_candidate_generator = recursive_select
elif _candidate_generator is None:
# By default, a tag's candidates are all of its
# children. If tag_name is defined, only yield tags
# with that name.
if self._select_debug:
if tag_name:
check = "[any]"
else:
check = tag_name
print ' Default candidate generator, tag name="%s"' % check
if self._select_debug:
# This is redundant with later code, but it stops
# a bunch of bogus tags from cluttering up the
# debug log.
def default_candidate_generator(tag):
for child in tag.descendants:
if not isinstance(child, Tag):
continue
if tag_name and not child.name == tag_name:
continue
yield child
_use_candidate_generator = default_candidate_generator
else:
_use_candidate_generator = lambda tag: tag.descendants
else:
_use_candidate_generator = _candidate_generator
for tag in current_context:
if self._select_debug:
print " Running candidate generator on %s %s" % (
tag.name, repr(tag.attrs))
for candidate in _use_candidate_generator(tag):
if not isinstance(candidate, Tag):
continue
if tag_name and candidate.name != tag_name:
continue
if checker is not None:
try:
result = checker(candidate)
except StopIteration:
# The checker has decided we should no longer
# run the generator.
break
if checker is None or result:
if self._select_debug:
print " SUCCESS %s %s" % (candidate.name, repr(candidate.attrs))
if id(candidate) not in new_context_ids:
# If a tag matches a selector more than once,
# don't include it in the context more than once.
new_context.append(candidate)
new_context_ids.add(id(candidate))
elif self._select_debug:
print " FAILURE %s %s" % (candidate.name, repr(candidate.attrs))
current_context = new_context

View file

@ -1,592 +0,0 @@
"""Helper classes for tests."""
import copy
import functools
import unittest
from unittest import TestCase
from bs4 import BeautifulSoup
from bs4.element import (
CharsetMetaAttributeValue,
Comment,
ContentMetaAttributeValue,
Doctype,
SoupStrainer,
)
from bs4.builder import HTMLParserTreeBuilder
default_builder = HTMLParserTreeBuilder
class SoupTest(unittest.TestCase):
@property
def default_builder(self):
return default_builder()
def soup(self, markup, **kwargs):
"""Build a Beautiful Soup object from markup."""
builder = kwargs.pop('builder', self.default_builder)
return BeautifulSoup(markup, builder=builder, **kwargs)
def document_for(self, markup):
"""Turn an HTML fragment into a document.
The details depend on the builder.
"""
return self.default_builder.test_fragment_to_document(markup)
def assertSoupEquals(self, to_parse, compare_parsed_to=None):
builder = self.default_builder
obj = BeautifulSoup(to_parse, builder=builder)
if compare_parsed_to is None:
compare_parsed_to = to_parse
self.assertEqual(obj.decode(), self.document_for(compare_parsed_to))
class HTMLTreeBuilderSmokeTest(object):
"""A basic test of a treebuilder's competence.
Any HTML treebuilder, present or future, should be able to pass
these tests. With invalid markup, there's room for interpretation,
and different parsers can handle it differently. But with the
markup in these tests, there's not much room for interpretation.
"""
def assertDoctypeHandled(self, doctype_fragment):
"""Assert that a given doctype string is handled correctly."""
doctype_str, soup = self._document_with_doctype(doctype_fragment)
# Make sure a Doctype object was created.
doctype = soup.contents[0]
self.assertEqual(doctype.__class__, Doctype)
self.assertEqual(doctype, doctype_fragment)
self.assertEqual(str(soup)[:len(doctype_str)], doctype_str)
# Make sure that the doctype was correctly associated with the
# parse tree and that the rest of the document parsed.
self.assertEqual(soup.p.contents[0], 'foo')
def _document_with_doctype(self, doctype_fragment):
"""Generate and parse a document with the given doctype."""
doctype = '<!DOCTYPE %s>' % doctype_fragment
markup = doctype + '\n<p>foo</p>'
soup = self.soup(markup)
return doctype, soup
def test_normal_doctypes(self):
"""Make sure normal, everyday HTML doctypes are handled correctly."""
self.assertDoctypeHandled("html")
self.assertDoctypeHandled(
'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"')
def test_empty_doctype(self):
soup = self.soup("<!DOCTYPE>")
doctype = soup.contents[0]
self.assertEqual("", doctype.strip())
def test_public_doctype_with_url(self):
doctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"'
self.assertDoctypeHandled(doctype)
def test_system_doctype(self):
self.assertDoctypeHandled('foo SYSTEM "http://www.example.com/"')
def test_namespaced_system_doctype(self):
# We can handle a namespaced doctype with a system ID.
self.assertDoctypeHandled('xsl:stylesheet SYSTEM "htmlent.dtd"')
def test_namespaced_public_doctype(self):
# Test a namespaced doctype with a public id.
self.assertDoctypeHandled('xsl:stylesheet PUBLIC "htmlent.dtd"')
def test_real_xhtml_document(self):
"""A real XHTML document should come out more or less the same as it went in."""
markup = b"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Hello.</title></head>
<body>Goodbye.</body>
</html>"""
soup = self.soup(markup)
self.assertEqual(
soup.encode("utf-8").replace(b"\n", b""),
markup.replace(b"\n", b""))
def test_deepcopy(self):
"""Make sure you can copy the tree builder.
This is important because the builder is part of a
BeautifulSoup object, and we want to be able to copy that.
"""
copy.deepcopy(self.default_builder)
def test_p_tag_is_never_empty_element(self):
"""A <p> tag is never designated as an empty-element tag.
Even if the markup shows it as an empty-element tag, it
shouldn't be presented that way.
"""
soup = self.soup("<p/>")
self.assertFalse(soup.p.is_empty_element)
self.assertEqual(str(soup.p), "<p></p>")
def test_unclosed_tags_get_closed(self):
"""A tag that's not closed by the end of the document should be closed.
This applies to all tags except empty-element tags.
"""
self.assertSoupEquals("<p>", "<p></p>")
self.assertSoupEquals("<b>", "<b></b>")
self.assertSoupEquals("<br>", "<br/>")
def test_br_is_always_empty_element_tag(self):
"""A <br> tag is designated as an empty-element tag.
Some parsers treat <br></br> as one <br/> tag, some parsers as
two tags, but it should always be an empty-element tag.
"""
soup = self.soup("<br></br>")
self.assertTrue(soup.br.is_empty_element)
self.assertEqual(str(soup.br), "<br/>")
def test_nested_formatting_elements(self):
self.assertSoupEquals("<em><em></em></em>")
def test_comment(self):
# Comments are represented as Comment objects.
markup = "<p>foo<!--foobar-->baz</p>"
self.assertSoupEquals(markup)
soup = self.soup(markup)
comment = soup.find(text="foobar")
self.assertEqual(comment.__class__, Comment)
# The comment is properly integrated into the tree.
foo = soup.find(text="foo")
self.assertEqual(comment, foo.next_element)
baz = soup.find(text="baz")
self.assertEqual(comment, baz.previous_element)
def test_preserved_whitespace_in_pre_and_textarea(self):
"""Whitespace must be preserved in <pre> and <textarea> tags."""
self.assertSoupEquals("<pre> </pre>")
self.assertSoupEquals("<textarea> woo </textarea>")
def test_nested_inline_elements(self):
"""Inline elements can be nested indefinitely."""
b_tag = "<b>Inside a B tag</b>"
self.assertSoupEquals(b_tag)
nested_b_tag = "<p>A <i>nested <b>tag</b></i></p>"
self.assertSoupEquals(nested_b_tag)
double_nested_b_tag = "<p>A <a>doubly <i>nested <b>tag</b></i></a></p>"
self.assertSoupEquals(nested_b_tag)
def test_nested_block_level_elements(self):
"""Block elements can be nested."""
soup = self.soup('<blockquote><p><b>Foo</b></p></blockquote>')
blockquote = soup.blockquote
self.assertEqual(blockquote.p.b.string, 'Foo')
self.assertEqual(blockquote.b.string, 'Foo')
def test_correctly_nested_tables(self):
"""One table can go inside another one."""
markup = ('<table id="1">'
'<tr>'
"<td>Here's another table:"
'<table id="2">'
'<tr><td>foo</td></tr>'
'</table></td>')
self.assertSoupEquals(
markup,
'<table id="1"><tr><td>Here\'s another table:'
'<table id="2"><tr><td>foo</td></tr></table>'
'</td></tr></table>')
self.assertSoupEquals(
"<table><thead><tr><td>Foo</td></tr></thead>"
"<tbody><tr><td>Bar</td></tr></tbody>"
"<tfoot><tr><td>Baz</td></tr></tfoot></table>")
def test_deeply_nested_multivalued_attribute(self):
# html5lib can set the attributes of the same tag many times
# as it rearranges the tree. This has caused problems with
# multivalued attributes.
markup = '<table><div><div class="css"></div></div></table>'
soup = self.soup(markup)
self.assertEqual(["css"], soup.div.div['class'])
def test_angle_brackets_in_attribute_values_are_escaped(self):
self.assertSoupEquals('<a b="<a>"></a>', '<a b="&lt;a&gt;"></a>')
def test_entities_in_attributes_converted_to_unicode(self):
expect = u'<p id="pi\N{LATIN SMALL LETTER N WITH TILDE}ata"></p>'
self.assertSoupEquals('<p id="pi&#241;ata"></p>', expect)
self.assertSoupEquals('<p id="pi&#xf1;ata"></p>', expect)
self.assertSoupEquals('<p id="pi&#Xf1;ata"></p>', expect)
self.assertSoupEquals('<p id="pi&ntilde;ata"></p>', expect)
def test_entities_in_text_converted_to_unicode(self):
expect = u'<p>pi\N{LATIN SMALL LETTER N WITH TILDE}ata</p>'
self.assertSoupEquals("<p>pi&#241;ata</p>", expect)
self.assertSoupEquals("<p>pi&#xf1;ata</p>", expect)
self.assertSoupEquals("<p>pi&#Xf1;ata</p>", expect)
self.assertSoupEquals("<p>pi&ntilde;ata</p>", expect)
def test_quot_entity_converted_to_quotation_mark(self):
self.assertSoupEquals("<p>I said &quot;good day!&quot;</p>",
'<p>I said "good day!"</p>')
def test_out_of_range_entity(self):
expect = u"\N{REPLACEMENT CHARACTER}"
self.assertSoupEquals("&#10000000000000;", expect)
self.assertSoupEquals("&#x10000000000000;", expect)
self.assertSoupEquals("&#1000000000;", expect)
def test_multipart_strings(self):
"Mostly to prevent a recurrence of a bug in the html5lib treebuilder."
soup = self.soup("<html><h2>\nfoo</h2><p></p></html>")
self.assertEqual("p", soup.h2.string.next_element.name)
self.assertEqual("p", soup.p.name)
def test_basic_namespaces(self):
"""Parsers don't need to *understand* namespaces, but at the
very least they should not choke on namespaces or lose
data."""
markup = b'<html xmlns="http://www.w3.org/1999/xhtml" xmlns:mathml="http://www.w3.org/1998/Math/MathML" xmlns:svg="http://www.w3.org/2000/svg"><head></head><body><mathml:msqrt>4</mathml:msqrt><b svg:fill="red"></b></body></html>'
soup = self.soup(markup)
self.assertEqual(markup, soup.encode())
html = soup.html
self.assertEqual('http://www.w3.org/1999/xhtml', soup.html['xmlns'])
self.assertEqual(
'http://www.w3.org/1998/Math/MathML', soup.html['xmlns:mathml'])
self.assertEqual(
'http://www.w3.org/2000/svg', soup.html['xmlns:svg'])
def test_multivalued_attribute_value_becomes_list(self):
markup = b'<a class="foo bar">'
soup = self.soup(markup)
self.assertEqual(['foo', 'bar'], soup.a['class'])
#
# Generally speaking, tests below this point are more tests of
# Beautiful Soup than tests of the tree builders. But parsers are
# weird, so we run these tests separately for every tree builder
# to detect any differences between them.
#
def test_can_parse_unicode_document(self):
# A seemingly innocuous document... but it's in Unicode! And
# it contains characters that can't be represented in the
# encoding found in the declaration! The horror!
markup = u'<html><head><meta encoding="euc-jp"></head><body>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</body>'
soup = self.soup(markup)
self.assertEqual(u'Sacr\xe9 bleu!', soup.body.string)
def test_soupstrainer(self):
"""Parsers should be able to work with SoupStrainers."""
strainer = SoupStrainer("b")
soup = self.soup("A <b>bold</b> <meta/> <i>statement</i>",
parse_only=strainer)
self.assertEqual(soup.decode(), "<b>bold</b>")
def test_single_quote_attribute_values_become_double_quotes(self):
self.assertSoupEquals("<foo attr='bar'></foo>",
'<foo attr="bar"></foo>')
def test_attribute_values_with_nested_quotes_are_left_alone(self):
text = """<foo attr='bar "brawls" happen'>a</foo>"""
self.assertSoupEquals(text)
def test_attribute_values_with_double_nested_quotes_get_quoted(self):
text = """<foo attr='bar "brawls" happen'>a</foo>"""
soup = self.soup(text)
soup.foo['attr'] = 'Brawls happen at "Bob\'s Bar"'
self.assertSoupEquals(
soup.foo.decode(),
"""<foo attr="Brawls happen at &quot;Bob\'s Bar&quot;">a</foo>""")
def test_ampersand_in_attribute_value_gets_escaped(self):
self.assertSoupEquals('<this is="really messed up & stuff"></this>',
'<this is="really messed up &amp; stuff"></this>')
self.assertSoupEquals(
'<a href="http://example.org?a=1&b=2;3">foo</a>',
'<a href="http://example.org?a=1&amp;b=2;3">foo</a>')
def test_escaped_ampersand_in_attribute_value_is_left_alone(self):
self.assertSoupEquals('<a href="http://example.org?a=1&amp;b=2;3"></a>')
def test_entities_in_strings_converted_during_parsing(self):
# Both XML and HTML entities are converted to Unicode characters
# during parsing.
text = "<p>&lt;&lt;sacr&eacute;&#32;bleu!&gt;&gt;</p>"
expected = u"<p>&lt;&lt;sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!&gt;&gt;</p>"
self.assertSoupEquals(text, expected)
def test_smart_quotes_converted_on_the_way_in(self):
# Microsoft smart quotes are converted to Unicode characters during
# parsing.
quote = b"<p>\x91Foo\x92</p>"
soup = self.soup(quote)
self.assertEqual(
soup.p.string,
u"\N{LEFT SINGLE QUOTATION MARK}Foo\N{RIGHT SINGLE QUOTATION MARK}")
def test_non_breaking_spaces_converted_on_the_way_in(self):
soup = self.soup("<a>&nbsp;&nbsp;</a>")
self.assertEqual(soup.a.string, u"\N{NO-BREAK SPACE}" * 2)
def test_entities_converted_on_the_way_out(self):
text = "<p>&lt;&lt;sacr&eacute;&#32;bleu!&gt;&gt;</p>"
expected = u"<p>&lt;&lt;sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!&gt;&gt;</p>".encode("utf-8")
soup = self.soup(text)
self.assertEqual(soup.p.encode("utf-8"), expected)
def test_real_iso_latin_document(self):
# Smoke test of interrelated functionality, using an
# easy-to-understand document.
# Here it is in Unicode. Note that it claims to be in ISO-Latin-1.
unicode_html = u'<html><head><meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type"/></head><body><p>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</p></body></html>'
# That's because we're going to encode it into ISO-Latin-1, and use
# that to test.
iso_latin_html = unicode_html.encode("iso-8859-1")
# Parse the ISO-Latin-1 HTML.
soup = self.soup(iso_latin_html)
# Encode it to UTF-8.
result = soup.encode("utf-8")
# What do we expect the result to look like? Well, it would
# look like unicode_html, except that the META tag would say
# UTF-8 instead of ISO-Latin-1.
expected = unicode_html.replace("ISO-Latin-1", "utf-8")
# And, of course, it would be in UTF-8, not Unicode.
expected = expected.encode("utf-8")
# Ta-da!
self.assertEqual(result, expected)
def test_real_shift_jis_document(self):
# Smoke test to make sure the parser can handle a document in
# Shift-JIS encoding, without choking.
shift_jis_html = (
b'<html><head></head><body><pre>'
b'\x82\xb1\x82\xea\x82\xcdShift-JIS\x82\xc5\x83R\x81[\x83f'
b'\x83B\x83\x93\x83O\x82\xb3\x82\xea\x82\xbd\x93\xfa\x96{\x8c'
b'\xea\x82\xcc\x83t\x83@\x83C\x83\x8b\x82\xc5\x82\xb7\x81B'
b'</pre></body></html>')
unicode_html = shift_jis_html.decode("shift-jis")
soup = self.soup(unicode_html)
# Make sure the parse tree is correctly encoded to various
# encodings.
self.assertEqual(soup.encode("utf-8"), unicode_html.encode("utf-8"))
self.assertEqual(soup.encode("euc_jp"), unicode_html.encode("euc_jp"))
def test_real_hebrew_document(self):
# A real-world test to make sure we can convert ISO-8859-9 (a
# Hebrew encoding) to UTF-8.
hebrew_document = b'<html><head><title>Hebrew (ISO 8859-8) in Visual Directionality</title></head><body><h1>Hebrew (ISO 8859-8) in Visual Directionality</h1>\xed\xe5\xec\xf9</body></html>'
soup = self.soup(
hebrew_document, from_encoding="iso8859-8")
self.assertEqual(soup.original_encoding, 'iso8859-8')
self.assertEqual(
soup.encode('utf-8'),
hebrew_document.decode("iso8859-8").encode("utf-8"))
def test_meta_tag_reflects_current_encoding(self):
# Here's the <meta> tag saying that a document is
# encoded in Shift-JIS.
meta_tag = ('<meta content="text/html; charset=x-sjis" '
'http-equiv="Content-type"/>')
# Here's a document incorporating that meta tag.
shift_jis_html = (
'<html><head>\n%s\n'
'<meta http-equiv="Content-language" content="ja"/>'
'</head><body>Shift-JIS markup goes here.') % meta_tag
soup = self.soup(shift_jis_html)
# Parse the document, and the charset is seemingly unaffected.
parsed_meta = soup.find('meta', {'http-equiv': 'Content-type'})
content = parsed_meta['content']
self.assertEqual('text/html; charset=x-sjis', content)
# But that value is actually a ContentMetaAttributeValue object.
self.assertTrue(isinstance(content, ContentMetaAttributeValue))
# And it will take on a value that reflects its current
# encoding.
self.assertEqual('text/html; charset=utf8', content.encode("utf8"))
# For the rest of the story, see TestSubstitutions in
# test_tree.py.
def test_html5_style_meta_tag_reflects_current_encoding(self):
# Here's the <meta> tag saying that a document is
# encoded in Shift-JIS.
meta_tag = ('<meta id="encoding" charset="x-sjis" />')
# Here's a document incorporating that meta tag.
shift_jis_html = (
'<html><head>\n%s\n'
'<meta http-equiv="Content-language" content="ja"/>'
'</head><body>Shift-JIS markup goes here.') % meta_tag
soup = self.soup(shift_jis_html)
# Parse the document, and the charset is seemingly unaffected.
parsed_meta = soup.find('meta', id="encoding")
charset = parsed_meta['charset']
self.assertEqual('x-sjis', charset)
# But that value is actually a CharsetMetaAttributeValue object.
self.assertTrue(isinstance(charset, CharsetMetaAttributeValue))
# And it will take on a value that reflects its current
# encoding.
self.assertEqual('utf8', charset.encode("utf8"))
def test_tag_with_no_attributes_can_have_attributes_added(self):
data = self.soup("<a>text</a>")
data.a['foo'] = 'bar'
self.assertEqual('<a foo="bar">text</a>', data.a.decode())
class XMLTreeBuilderSmokeTest(object):
def test_docstring_generated(self):
soup = self.soup("<root/>")
self.assertEqual(
soup.encode(), b'<?xml version="1.0" encoding="utf-8"?>\n<root/>')
def test_real_xhtml_document(self):
"""A real XHTML document should come out *exactly* the same as it went in."""
markup = b"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Hello.</title></head>
<body>Goodbye.</body>
</html>"""
soup = self.soup(markup)
self.assertEqual(
soup.encode("utf-8"), markup)
def test_formatter_processes_script_tag_for_xml_documents(self):
doc = """
<script type="text/javascript">
</script>
"""
soup = BeautifulSoup(doc, "xml")
# lxml would have stripped this while parsing, but we can add
# it later.
soup.script.string = 'console.log("< < hey > > ");'
encoded = soup.encode()
self.assertTrue(b"&lt; &lt; hey &gt; &gt;" in encoded)
def test_can_parse_unicode_document(self):
markup = u'<?xml version="1.0" encoding="euc-jp"><root>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</root>'
soup = self.soup(markup)
self.assertEqual(u'Sacr\xe9 bleu!', soup.root.string)
def test_popping_namespaced_tag(self):
markup = '<rss xmlns:dc="foo"><dc:creator>b</dc:creator><dc:date>2012-07-02T20:33:42Z</dc:date><dc:rights>c</dc:rights><image>d</image></rss>'
soup = self.soup(markup)
self.assertEqual(
unicode(soup.rss), markup)
def test_docstring_includes_correct_encoding(self):
soup = self.soup("<root/>")
self.assertEqual(
soup.encode("latin1"),
b'<?xml version="1.0" encoding="latin1"?>\n<root/>')
def test_large_xml_document(self):
"""A large XML document should come out the same as it went in."""
markup = (b'<?xml version="1.0" encoding="utf-8"?>\n<root>'
+ b'0' * (2**12)
+ b'</root>')
soup = self.soup(markup)
self.assertEqual(soup.encode("utf-8"), markup)
def test_tags_are_empty_element_if_and_only_if_they_are_empty(self):
self.assertSoupEquals("<p>", "<p/>")
self.assertSoupEquals("<p>foo</p>")
def test_namespaces_are_preserved(self):
markup = '<root xmlns:a="http://example.com/" xmlns:b="http://example.net/"><a:foo>This tag is in the a namespace</a:foo><b:foo>This tag is in the b namespace</b:foo></root>'
soup = self.soup(markup)
root = soup.root
self.assertEqual("http://example.com/", root['xmlns:a'])
self.assertEqual("http://example.net/", root['xmlns:b'])
def test_closing_namespaced_tag(self):
markup = '<p xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>20010504</dc:date></p>'
soup = self.soup(markup)
self.assertEqual(unicode(soup.p), markup)
def test_namespaced_attributes(self):
markup = '<foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><bar xsi:schemaLocation="http://www.example.com"/></foo>'
soup = self.soup(markup)
self.assertEqual(unicode(soup.foo), markup)
def test_namespaced_attributes_xml_namespace(self):
markup = '<foo xml:lang="fr">bar</foo>'
soup = self.soup(markup)
self.assertEqual(unicode(soup.foo), markup)
class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest):
"""Smoke test for a tree builder that supports HTML5."""
def test_real_xhtml_document(self):
# Since XHTML is not HTML5, HTML5 parsers are not tested to handle
# XHTML documents in any particular way.
pass
def test_html_tags_have_namespace(self):
markup = "<a>"
soup = self.soup(markup)
self.assertEqual("http://www.w3.org/1999/xhtml", soup.a.namespace)
def test_svg_tags_have_namespace(self):
markup = '<svg><circle/></svg>'
soup = self.soup(markup)
namespace = "http://www.w3.org/2000/svg"
self.assertEqual(namespace, soup.svg.namespace)
self.assertEqual(namespace, soup.circle.namespace)
def test_mathml_tags_have_namespace(self):
markup = '<math><msqrt>5</msqrt></math>'
soup = self.soup(markup)
namespace = 'http://www.w3.org/1998/Math/MathML'
self.assertEqual(namespace, soup.math.namespace)
self.assertEqual(namespace, soup.msqrt.namespace)
def test_xml_declaration_becomes_comment(self):
markup = '<?xml version="1.0" encoding="utf-8"?><html></html>'
soup = self.soup(markup)
self.assertTrue(isinstance(soup.contents[0], Comment))
self.assertEqual(soup.contents[0], '?xml version="1.0" encoding="utf-8"?')
self.assertEqual("html", soup.contents[0].next_element.name)
def skipIf(condition, reason):
def nothing(test, *args, **kwargs):
return None
def decorator(test_item):
if condition:
return nothing
else:
return test_item
return decorator

View file

@ -1 +0,0 @@
"The beautifulsoup tests."

View file

@ -1,141 +0,0 @@
"""Tests of the builder registry."""
import unittest
from bs4 import BeautifulSoup
from bs4.builder import (
builder_registry as registry,
HTMLParserTreeBuilder,
TreeBuilderRegistry,
)
try:
from bs4.builder import HTML5TreeBuilder
HTML5LIB_PRESENT = True
except ImportError:
HTML5LIB_PRESENT = False
try:
from bs4.builder import (
LXMLTreeBuilderForXML,
LXMLTreeBuilder,
)
LXML_PRESENT = True
except ImportError:
LXML_PRESENT = False
class BuiltInRegistryTest(unittest.TestCase):
"""Test the built-in registry with the default builders registered."""
def test_combination(self):
if LXML_PRESENT:
self.assertEqual(registry.lookup('fast', 'html'),
LXMLTreeBuilder)
if LXML_PRESENT:
self.assertEqual(registry.lookup('permissive', 'xml'),
LXMLTreeBuilderForXML)
self.assertEqual(registry.lookup('strict', 'html'),
HTMLParserTreeBuilder)
if HTML5LIB_PRESENT:
self.assertEqual(registry.lookup('html5lib', 'html'),
HTML5TreeBuilder)
def test_lookup_by_markup_type(self):
if LXML_PRESENT:
self.assertEqual(registry.lookup('html'), LXMLTreeBuilder)
self.assertEqual(registry.lookup('xml'), LXMLTreeBuilderForXML)
else:
self.assertEqual(registry.lookup('xml'), None)
if HTML5LIB_PRESENT:
self.assertEqual(registry.lookup('html'), HTML5TreeBuilder)
else:
self.assertEqual(registry.lookup('html'), HTMLParserTreeBuilder)
def test_named_library(self):
if LXML_PRESENT:
self.assertEqual(registry.lookup('lxml', 'xml'),
LXMLTreeBuilderForXML)
self.assertEqual(registry.lookup('lxml', 'html'),
LXMLTreeBuilder)
if HTML5LIB_PRESENT:
self.assertEqual(registry.lookup('html5lib'),
HTML5TreeBuilder)
self.assertEqual(registry.lookup('html.parser'),
HTMLParserTreeBuilder)
def test_beautifulsoup_constructor_does_lookup(self):
# You can pass in a string.
BeautifulSoup("", features="html")
# Or a list of strings.
BeautifulSoup("", features=["html", "fast"])
# You'll get an exception if BS can't find an appropriate
# builder.
self.assertRaises(ValueError, BeautifulSoup,
"", features="no-such-feature")
class RegistryTest(unittest.TestCase):
"""Test the TreeBuilderRegistry class in general."""
def setUp(self):
self.registry = TreeBuilderRegistry()
def builder_for_features(self, *feature_list):
cls = type('Builder_' + '_'.join(feature_list),
(object,), {'features' : feature_list})
self.registry.register(cls)
return cls
def test_register_with_no_features(self):
builder = self.builder_for_features()
# Since the builder advertises no features, you can't find it
# by looking up features.
self.assertEqual(self.registry.lookup('foo'), None)
# But you can find it by doing a lookup with no features, if
# this happens to be the only registered builder.
self.assertEqual(self.registry.lookup(), builder)
def test_register_with_features_makes_lookup_succeed(self):
builder = self.builder_for_features('foo', 'bar')
self.assertEqual(self.registry.lookup('foo'), builder)
self.assertEqual(self.registry.lookup('bar'), builder)
def test_lookup_fails_when_no_builder_implements_feature(self):
builder = self.builder_for_features('foo', 'bar')
self.assertEqual(self.registry.lookup('baz'), None)
def test_lookup_gets_most_recent_registration_when_no_feature_specified(self):
builder1 = self.builder_for_features('foo')
builder2 = self.builder_for_features('bar')
self.assertEqual(self.registry.lookup(), builder2)
def test_lookup_fails_when_no_tree_builders_registered(self):
self.assertEqual(self.registry.lookup(), None)
def test_lookup_gets_most_recent_builder_supporting_all_features(self):
has_one = self.builder_for_features('foo')
has_the_other = self.builder_for_features('bar')
has_both_early = self.builder_for_features('foo', 'bar', 'baz')
has_both_late = self.builder_for_features('foo', 'bar', 'quux')
lacks_one = self.builder_for_features('bar')
has_the_other = self.builder_for_features('foo')
# There are two builders featuring 'foo' and 'bar', but
# the one that also features 'quux' was registered later.
self.assertEqual(self.registry.lookup('foo', 'bar'),
has_both_late)
# There is only one builder featuring 'foo', 'bar', and 'baz'.
self.assertEqual(self.registry.lookup('foo', 'bar', 'baz'),
has_both_early)
def test_lookup_fails_when_cannot_reconcile_requested_features(self):
builder1 = self.builder_for_features('foo', 'bar')
builder2 = self.builder_for_features('foo', 'baz')
self.assertEqual(self.registry.lookup('bar', 'baz'), None)

View file

@ -1,36 +0,0 @@
"Test harness for doctests."
# pylint: disable-msg=E0611,W0142
__metaclass__ = type
__all__ = [
'additional_tests',
]
import atexit
import doctest
import os
#from pkg_resources import (
# resource_filename, resource_exists, resource_listdir, cleanup_resources)
import unittest
DOCTEST_FLAGS = (
doctest.ELLIPSIS |
doctest.NORMALIZE_WHITESPACE |
doctest.REPORT_NDIFF)
# def additional_tests():
# "Run the doc tests (README.txt and docs/*, if any exist)"
# doctest_files = [
# os.path.abspath(resource_filename('bs4', 'README.txt'))]
# if resource_exists('bs4', 'docs'):
# for name in resource_listdir('bs4', 'docs'):
# if name.endswith('.txt'):
# doctest_files.append(
# os.path.abspath(
# resource_filename('bs4', 'docs/%s' % name)))
# kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS)
# atexit.register(cleanup_resources)
# return unittest.TestSuite((
# doctest.DocFileSuite(*doctest_files, **kwargs)))

View file

@ -1,85 +0,0 @@
"""Tests to ensure that the html5lib tree builder generates good trees."""
import warnings
try:
from bs4.builder import HTML5TreeBuilder
HTML5LIB_PRESENT = True
except ImportError, e:
HTML5LIB_PRESENT = False
from bs4.element import SoupStrainer
from bs4.testing import (
HTML5TreeBuilderSmokeTest,
SoupTest,
skipIf,
)
@skipIf(
not HTML5LIB_PRESENT,
"html5lib seems not to be present, not testing its tree builder.")
class HTML5LibBuilderSmokeTest(SoupTest, HTML5TreeBuilderSmokeTest):
"""See ``HTML5TreeBuilderSmokeTest``."""
@property
def default_builder(self):
return HTML5TreeBuilder()
def test_soupstrainer(self):
# The html5lib tree builder does not support SoupStrainers.
strainer = SoupStrainer("b")
markup = "<p>A <b>bold</b> statement.</p>"
with warnings.catch_warnings(record=True) as w:
soup = self.soup(markup, parse_only=strainer)
self.assertEqual(
soup.decode(), self.document_for(markup))
self.assertTrue(
"the html5lib tree builder doesn't support parse_only" in
str(w[0].message))
def test_correctly_nested_tables(self):
"""html5lib inserts <tbody> tags where other parsers don't."""
markup = ('<table id="1">'
'<tr>'
"<td>Here's another table:"
'<table id="2">'
'<tr><td>foo</td></tr>'
'</table></td>')
self.assertSoupEquals(
markup,
'<table id="1"><tbody><tr><td>Here\'s another table:'
'<table id="2"><tbody><tr><td>foo</td></tr></tbody></table>'
'</td></tr></tbody></table>')
self.assertSoupEquals(
"<table><thead><tr><td>Foo</td></tr></thead>"
"<tbody><tr><td>Bar</td></tr></tbody>"
"<tfoot><tr><td>Baz</td></tr></tfoot></table>")
def test_xml_declaration_followed_by_doctype(self):
markup = '''<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p>foo</p>
</body>
</html>'''
soup = self.soup(markup)
# Verify that we can reach the <p> tag; this means the tree is connected.
self.assertEqual(b"<p>foo</p>", soup.p.encode())
def test_reparented_markup(self):
markup = '<p><em>foo</p>\n<p>bar<a></a></em></p>'
soup = self.soup(markup)
self.assertEqual(u"<body><p><em>foo</em></p><em>\n</em><p><em>bar<a></a></em></p></body>", soup.body.decode())
self.assertEqual(2, len(soup.find_all('p')))
def test_reparented_markup_ends_with_whitespace(self):
markup = '<p><em>foo</p>\n<p>bar<a></a></em></p>\n'
soup = self.soup(markup)
self.assertEqual(u"<body><p><em>foo</em></p><em>\n</em><p><em>bar<a></a></em></p>\n</body>", soup.body.decode())
self.assertEqual(2, len(soup.find_all('p')))

View file

@ -1,19 +0,0 @@
"""Tests to ensure that the html.parser tree builder generates good
trees."""
from bs4.testing import SoupTest, HTMLTreeBuilderSmokeTest
from bs4.builder import HTMLParserTreeBuilder
class HTMLParserTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest):
@property
def default_builder(self):
return HTMLParserTreeBuilder()
def test_namespaced_system_doctype(self):
# html.parser can't handle namespaced doctypes, so skip this one.
pass
def test_namespaced_public_doctype(self):
# html.parser can't handle namespaced doctypes, so skip this one.
pass

View file

@ -1,91 +0,0 @@
"""Tests to ensure that the lxml tree builder generates good trees."""
import re
import warnings
try:
import lxml.etree
LXML_PRESENT = True
LXML_VERSION = lxml.etree.LXML_VERSION
except ImportError, e:
LXML_PRESENT = False
LXML_VERSION = (0,)
if LXML_PRESENT:
from bs4.builder import LXMLTreeBuilder, LXMLTreeBuilderForXML
from bs4 import (
BeautifulSoup,
BeautifulStoneSoup,
)
from bs4.element import Comment, Doctype, SoupStrainer
from bs4.testing import skipIf
from bs4.tests import test_htmlparser
from bs4.testing import (
HTMLTreeBuilderSmokeTest,
XMLTreeBuilderSmokeTest,
SoupTest,
skipIf,
)
@skipIf(
not LXML_PRESENT,
"lxml seems not to be present, not testing its tree builder.")
class LXMLTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest):
"""See ``HTMLTreeBuilderSmokeTest``."""
@property
def default_builder(self):
return LXMLTreeBuilder()
def test_out_of_range_entity(self):
self.assertSoupEquals(
"<p>foo&#10000000000000;bar</p>", "<p>foobar</p>")
self.assertSoupEquals(
"<p>foo&#x10000000000000;bar</p>", "<p>foobar</p>")
self.assertSoupEquals(
"<p>foo&#1000000000;bar</p>", "<p>foobar</p>")
# In lxml < 2.3.5, an empty doctype causes a segfault. Skip this
# test if an old version of lxml is installed.
@skipIf(
not LXML_PRESENT or LXML_VERSION < (2,3,5,0),
"Skipping doctype test for old version of lxml to avoid segfault.")
def test_empty_doctype(self):
soup = self.soup("<!DOCTYPE>")
doctype = soup.contents[0]
self.assertEqual("", doctype.strip())
def test_beautifulstonesoup_is_xml_parser(self):
# Make sure that the deprecated BSS class uses an xml builder
# if one is installed.
with warnings.catch_warnings(record=True) as w:
soup = BeautifulStoneSoup("<b />")
self.assertEqual(u"<b/>", unicode(soup.b))
self.assertTrue("BeautifulStoneSoup class is deprecated" in str(w[0].message))
def test_real_xhtml_document(self):
"""lxml strips the XML definition from an XHTML doc, which is fine."""
markup = b"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Hello.</title></head>
<body>Goodbye.</body>
</html>"""
soup = self.soup(markup)
self.assertEqual(
soup.encode("utf-8").replace(b"\n", b''),
markup.replace(b'\n', b'').replace(
b'<?xml version="1.0" encoding="utf-8"?>', b''))
@skipIf(
not LXML_PRESENT,
"lxml seems not to be present, not testing its XML tree builder.")
class LXMLXMLTreeBuilderSmokeTest(SoupTest, XMLTreeBuilderSmokeTest):
"""See ``HTMLTreeBuilderSmokeTest``."""
@property
def default_builder(self):
return LXMLTreeBuilderForXML()

View file

@ -1,434 +0,0 @@
# -*- coding: utf-8 -*-
"""Tests of Beautiful Soup as a whole."""
import logging
import unittest
import sys
import tempfile
from bs4 import (
BeautifulSoup,
BeautifulStoneSoup,
)
from bs4.element import (
CharsetMetaAttributeValue,
ContentMetaAttributeValue,
SoupStrainer,
NamespacedAttribute,
)
import bs4.dammit
from bs4.dammit import (
EntitySubstitution,
UnicodeDammit,
)
from bs4.testing import (
SoupTest,
skipIf,
)
import warnings
try:
from bs4.builder import LXMLTreeBuilder, LXMLTreeBuilderForXML
LXML_PRESENT = True
except ImportError, e:
LXML_PRESENT = False
PYTHON_2_PRE_2_7 = (sys.version_info < (2,7))
PYTHON_3_PRE_3_2 = (sys.version_info[0] == 3 and sys.version_info < (3,2))
class TestConstructor(SoupTest):
def test_short_unicode_input(self):
data = u"<h1>éé</h1>"
soup = self.soup(data)
self.assertEqual(u"éé", soup.h1.string)
def test_embedded_null(self):
data = u"<h1>foo\0bar</h1>"
soup = self.soup(data)
self.assertEqual(u"foo\0bar", soup.h1.string)
class TestDeprecatedConstructorArguments(SoupTest):
def test_parseOnlyThese_renamed_to_parse_only(self):
with warnings.catch_warnings(record=True) as w:
soup = self.soup("<a><b></b></a>", parseOnlyThese=SoupStrainer("b"))
msg = str(w[0].message)
self.assertTrue("parseOnlyThese" in msg)
self.assertTrue("parse_only" in msg)
self.assertEqual(b"<b></b>", soup.encode())
def test_fromEncoding_renamed_to_from_encoding(self):
with warnings.catch_warnings(record=True) as w:
utf8 = b"\xc3\xa9"
soup = self.soup(utf8, fromEncoding="utf8")
msg = str(w[0].message)
self.assertTrue("fromEncoding" in msg)
self.assertTrue("from_encoding" in msg)
self.assertEqual("utf8", soup.original_encoding)
def test_unrecognized_keyword_argument(self):
self.assertRaises(
TypeError, self.soup, "<a>", no_such_argument=True)
class TestWarnings(SoupTest):
def test_disk_file_warning(self):
filehandle = tempfile.NamedTemporaryFile()
filename = filehandle.name
try:
with warnings.catch_warnings(record=True) as w:
soup = self.soup(filename)
msg = str(w[0].message)
self.assertTrue("looks like a filename" in msg)
finally:
filehandle.close()
# The file no longer exists, so Beautiful Soup will no longer issue the warning.
with warnings.catch_warnings(record=True) as w:
soup = self.soup(filename)
self.assertEqual(0, len(w))
def test_url_warning(self):
with warnings.catch_warnings(record=True) as w:
soup = self.soup("http://www.crummy.com/")
msg = str(w[0].message)
self.assertTrue("looks like a URL" in msg)
with warnings.catch_warnings(record=True) as w:
soup = self.soup("http://www.crummy.com/ is great")
self.assertEqual(0, len(w))
class TestSelectiveParsing(SoupTest):
def test_parse_with_soupstrainer(self):
markup = "No<b>Yes</b><a>No<b>Yes <c>Yes</c></b>"
strainer = SoupStrainer("b")
soup = self.soup(markup, parse_only=strainer)
self.assertEqual(soup.encode(), b"<b>Yes</b><b>Yes <c>Yes</c></b>")
class TestEntitySubstitution(unittest.TestCase):
"""Standalone tests of the EntitySubstitution class."""
def setUp(self):
self.sub = EntitySubstitution
def test_simple_html_substitution(self):
# Unicode characters corresponding to named HTML entites
# are substituted, and no others.
s = u"foo\u2200\N{SNOWMAN}\u00f5bar"
self.assertEqual(self.sub.substitute_html(s),
u"foo&forall;\N{SNOWMAN}&otilde;bar")
def test_smart_quote_substitution(self):
# MS smart quotes are a common source of frustration, so we
# give them a special test.
quotes = b"\x91\x92foo\x93\x94"
dammit = UnicodeDammit(quotes)
self.assertEqual(self.sub.substitute_html(dammit.markup),
"&lsquo;&rsquo;foo&ldquo;&rdquo;")
def test_xml_converstion_includes_no_quotes_if_make_quoted_attribute_is_false(self):
s = 'Welcome to "my bar"'
self.assertEqual(self.sub.substitute_xml(s, False), s)
def test_xml_attribute_quoting_normally_uses_double_quotes(self):
self.assertEqual(self.sub.substitute_xml("Welcome", True),
'"Welcome"')
self.assertEqual(self.sub.substitute_xml("Bob's Bar", True),
'"Bob\'s Bar"')
def test_xml_attribute_quoting_uses_single_quotes_when_value_contains_double_quotes(self):
s = 'Welcome to "my bar"'
self.assertEqual(self.sub.substitute_xml(s, True),
"'Welcome to \"my bar\"'")
def test_xml_attribute_quoting_escapes_single_quotes_when_value_contains_both_single_and_double_quotes(self):
s = 'Welcome to "Bob\'s Bar"'
self.assertEqual(
self.sub.substitute_xml(s, True),
'"Welcome to &quot;Bob\'s Bar&quot;"')
def test_xml_quotes_arent_escaped_when_value_is_not_being_quoted(self):
quoted = 'Welcome to "Bob\'s Bar"'
self.assertEqual(self.sub.substitute_xml(quoted), quoted)
def test_xml_quoting_handles_angle_brackets(self):
self.assertEqual(
self.sub.substitute_xml("foo<bar>"),
"foo&lt;bar&gt;")
def test_xml_quoting_handles_ampersands(self):
self.assertEqual(self.sub.substitute_xml("AT&T"), "AT&amp;T")
def test_xml_quoting_including_ampersands_when_they_are_part_of_an_entity(self):
self.assertEqual(
self.sub.substitute_xml("&Aacute;T&T"),
"&amp;Aacute;T&amp;T")
def test_xml_quoting_ignoring_ampersands_when_they_are_part_of_an_entity(self):
self.assertEqual(
self.sub.substitute_xml_containing_entities("&Aacute;T&T"),
"&Aacute;T&amp;T")
def test_quotes_not_html_substituted(self):
"""There's no need to do this except inside attribute values."""
text = 'Bob\'s "bar"'
self.assertEqual(self.sub.substitute_html(text), text)
class TestEncodingConversion(SoupTest):
# Test Beautiful Soup's ability to decode and encode from various
# encodings.
def setUp(self):
super(TestEncodingConversion, self).setUp()
self.unicode_data = u'<html><head><meta charset="utf-8"/></head><body><foo>Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!</foo></body></html>'
self.utf8_data = self.unicode_data.encode("utf-8")
# Just so you know what it looks like.
self.assertEqual(
self.utf8_data,
b'<html><head><meta charset="utf-8"/></head><body><foo>Sacr\xc3\xa9 bleu!</foo></body></html>')
def test_ascii_in_unicode_out(self):
# ASCII input is converted to Unicode. The original_encoding
# attribute is set to 'utf-8', a superset of ASCII.
chardet = bs4.dammit.chardet_dammit
logging.disable(logging.WARNING)
try:
def noop(str):
return None
# Disable chardet, which will realize that the ASCII is ASCII.
bs4.dammit.chardet_dammit = noop
ascii = b"<foo>a</foo>"
soup_from_ascii = self.soup(ascii)
unicode_output = soup_from_ascii.decode()
self.assertTrue(isinstance(unicode_output, unicode))
self.assertEqual(unicode_output, self.document_for(ascii.decode()))
self.assertEqual(soup_from_ascii.original_encoding.lower(), "utf-8")
finally:
logging.disable(logging.NOTSET)
bs4.dammit.chardet_dammit = chardet
def test_unicode_in_unicode_out(self):
# Unicode input is left alone. The original_encoding attribute
# is not set.
soup_from_unicode = self.soup(self.unicode_data)
self.assertEqual(soup_from_unicode.decode(), self.unicode_data)
self.assertEqual(soup_from_unicode.foo.string, u'Sacr\xe9 bleu!')
self.assertEqual(soup_from_unicode.original_encoding, None)
def test_utf8_in_unicode_out(self):
# UTF-8 input is converted to Unicode. The original_encoding
# attribute is set.
soup_from_utf8 = self.soup(self.utf8_data)
self.assertEqual(soup_from_utf8.decode(), self.unicode_data)
self.assertEqual(soup_from_utf8.foo.string, u'Sacr\xe9 bleu!')
def test_utf8_out(self):
# The internal data structures can be encoded as UTF-8.
soup_from_unicode = self.soup(self.unicode_data)
self.assertEqual(soup_from_unicode.encode('utf-8'), self.utf8_data)
@skipIf(
PYTHON_2_PRE_2_7 or PYTHON_3_PRE_3_2,
"Bad HTMLParser detected; skipping test of non-ASCII characters in attribute name.")
def test_attribute_name_containing_unicode_characters(self):
markup = u'<div><a \N{SNOWMAN}="snowman"></a></div>'
self.assertEqual(self.soup(markup).div.encode("utf8"), markup.encode("utf8"))
class TestUnicodeDammit(unittest.TestCase):
"""Standalone tests of UnicodeDammit."""
def test_unicode_input(self):
markup = u"I'm already Unicode! \N{SNOWMAN}"
dammit = UnicodeDammit(markup)
self.assertEqual(dammit.unicode_markup, markup)
def test_smart_quotes_to_unicode(self):
markup = b"<foo>\x91\x92\x93\x94</foo>"
dammit = UnicodeDammit(markup)
self.assertEqual(
dammit.unicode_markup, u"<foo>\u2018\u2019\u201c\u201d</foo>")
def test_smart_quotes_to_xml_entities(self):
markup = b"<foo>\x91\x92\x93\x94</foo>"
dammit = UnicodeDammit(markup, smart_quotes_to="xml")
self.assertEqual(
dammit.unicode_markup, "<foo>&#x2018;&#x2019;&#x201C;&#x201D;</foo>")
def test_smart_quotes_to_html_entities(self):
markup = b"<foo>\x91\x92\x93\x94</foo>"
dammit = UnicodeDammit(markup, smart_quotes_to="html")
self.assertEqual(
dammit.unicode_markup, "<foo>&lsquo;&rsquo;&ldquo;&rdquo;</foo>")
def test_smart_quotes_to_ascii(self):
markup = b"<foo>\x91\x92\x93\x94</foo>"
dammit = UnicodeDammit(markup, smart_quotes_to="ascii")
self.assertEqual(
dammit.unicode_markup, """<foo>''""</foo>""")
def test_detect_utf8(self):
utf8 = b"\xc3\xa9"
dammit = UnicodeDammit(utf8)
self.assertEqual(dammit.unicode_markup, u'\xe9')
self.assertEqual(dammit.original_encoding.lower(), 'utf-8')
def test_convert_hebrew(self):
hebrew = b"\xed\xe5\xec\xf9"
dammit = UnicodeDammit(hebrew, ["iso-8859-8"])
self.assertEqual(dammit.original_encoding.lower(), 'iso-8859-8')
self.assertEqual(dammit.unicode_markup, u'\u05dd\u05d5\u05dc\u05e9')
def test_dont_see_smart_quotes_where_there_are_none(self):
utf_8 = b"\343\202\261\343\203\274\343\202\277\343\202\244 Watch"
dammit = UnicodeDammit(utf_8)
self.assertEqual(dammit.original_encoding.lower(), 'utf-8')
self.assertEqual(dammit.unicode_markup.encode("utf-8"), utf_8)
def test_ignore_inappropriate_codecs(self):
utf8_data = u"Räksmörgås".encode("utf-8")
dammit = UnicodeDammit(utf8_data, ["iso-8859-8"])
self.assertEqual(dammit.original_encoding.lower(), 'utf-8')
def test_ignore_invalid_codecs(self):
utf8_data = u"Räksmörgås".encode("utf-8")
for bad_encoding in ['.utf8', '...', 'utF---16.!']:
dammit = UnicodeDammit(utf8_data, [bad_encoding])
self.assertEqual(dammit.original_encoding.lower(), 'utf-8')
def test_detect_html5_style_meta_tag(self):
for data in (
b'<html><meta charset="euc-jp" /></html>',
b"<html><meta charset='euc-jp' /></html>",
b"<html><meta charset=euc-jp /></html>",
b"<html><meta charset=euc-jp/></html>"):
dammit = UnicodeDammit(data, is_html=True)
self.assertEqual(
"euc-jp", dammit.original_encoding)
def test_last_ditch_entity_replacement(self):
# This is a UTF-8 document that contains bytestrings
# completely incompatible with UTF-8 (ie. encoded with some other
# encoding).
#
# Since there is no consistent encoding for the document,
# Unicode, Dammit will eventually encode the document as UTF-8
# and encode the incompatible characters as REPLACEMENT
# CHARACTER.
#
# If chardet is installed, it will detect that the document
# can be converted into ISO-8859-1 without errors. This happens
# to be the wrong encoding, but it is a consistent encoding, so the
# code we're testing here won't run.
#
# So we temporarily disable chardet if it's present.
doc = b"""\357\273\277<?xml version="1.0" encoding="UTF-8"?>
<html><b>\330\250\330\252\330\261</b>
<i>\310\322\321\220\312\321\355\344</i></html>"""
chardet = bs4.dammit.chardet_dammit
logging.disable(logging.WARNING)
try:
def noop(str):
return None
bs4.dammit.chardet_dammit = noop
dammit = UnicodeDammit(doc)
self.assertEqual(True, dammit.contains_replacement_characters)
self.assertTrue(u"\ufffd" in dammit.unicode_markup)
soup = BeautifulSoup(doc, "html.parser")
self.assertTrue(soup.contains_replacement_characters)
finally:
logging.disable(logging.NOTSET)
bs4.dammit.chardet_dammit = chardet
def test_byte_order_mark_removed(self):
# A document written in UTF-16LE will have its byte order marker stripped.
data = b'\xff\xfe<\x00a\x00>\x00\xe1\x00\xe9\x00<\x00/\x00a\x00>\x00'
dammit = UnicodeDammit(data)
self.assertEqual(u"<a>áé</a>", dammit.unicode_markup)
self.assertEqual("utf-16le", dammit.original_encoding)
def test_detwingle(self):
# Here's a UTF8 document.
utf8 = (u"\N{SNOWMAN}" * 3).encode("utf8")
# Here's a Windows-1252 document.
windows_1252 = (
u"\N{LEFT DOUBLE QUOTATION MARK}Hi, I like Windows!"
u"\N{RIGHT DOUBLE QUOTATION MARK}").encode("windows_1252")
# Through some unholy alchemy, they've been stuck together.
doc = utf8 + windows_1252 + utf8
# The document can't be turned into UTF-8:
self.assertRaises(UnicodeDecodeError, doc.decode, "utf8")
# Unicode, Dammit thinks the whole document is Windows-1252,
# and decodes it into "☃☃☃“Hi, I like Windows!”☃☃☃"
# But if we run it through fix_embedded_windows_1252, it's fixed:
fixed = UnicodeDammit.detwingle(doc)
self.assertEqual(
u"☃☃☃“Hi, I like Windows!”☃☃☃", fixed.decode("utf8"))
def test_detwingle_ignores_multibyte_characters(self):
# Each of these characters has a UTF-8 representation ending
# in \x93. \x93 is a smart quote if interpreted as
# Windows-1252. But our code knows to skip over multibyte
# UTF-8 characters, so they'll survive the process unscathed.
for tricky_unicode_char in (
u"\N{LATIN SMALL LIGATURE OE}", # 2-byte char '\xc5\x93'
u"\N{LATIN SUBSCRIPT SMALL LETTER X}", # 3-byte char '\xe2\x82\x93'
u"\xf0\x90\x90\x93", # This is a CJK character, not sure which one.
):
input = tricky_unicode_char.encode("utf8")
self.assertTrue(input.endswith(b'\x93'))
output = UnicodeDammit.detwingle(input)
self.assertEqual(output, input)
class TestNamedspacedAttribute(SoupTest):
def test_name_may_be_none(self):
a = NamespacedAttribute("xmlns", None)
self.assertEqual(a, "xmlns")
def test_attribute_is_equivalent_to_colon_separated_string(self):
a = NamespacedAttribute("a", "b")
self.assertEqual("a:b", a)
def test_attributes_are_equivalent_if_prefix_and_name_identical(self):
a = NamespacedAttribute("a", "b", "c")
b = NamespacedAttribute("a", "b", "c")
self.assertEqual(a, b)
# The actual namespace is not considered.
c = NamespacedAttribute("a", "b", None)
self.assertEqual(a, c)
# But name and prefix are important.
d = NamespacedAttribute("a", "z", "c")
self.assertNotEqual(a, d)
e = NamespacedAttribute("z", "b", "c")
self.assertNotEqual(a, e)
class TestAttributeValueWithCharsetSubstitution(unittest.TestCase):
def test_content_meta_attribute_value(self):
value = CharsetMetaAttributeValue("euc-jp")
self.assertEqual("euc-jp", value)
self.assertEqual("euc-jp", value.original_value)
self.assertEqual("utf8", value.encode("utf8"))
def test_content_meta_attribute_value(self):
value = ContentMetaAttributeValue("text/html; charset=euc-jp")
self.assertEqual("text/html; charset=euc-jp", value)
self.assertEqual("text/html; charset=euc-jp", value.original_value)
self.assertEqual("text/html; charset=utf8", value.encode("utf8"))

File diff suppressed because it is too large Load diff

View file

@ -90,7 +90,7 @@ class FileCache(BaseCache):
def delete(self, key):
name = self._fn(key)
if not self.forever:
if not self.forever and os.path.exists(name):
os.remove(name)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
__version__ = '5.1.0'

1471
lib/configobj/validate.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,2 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard Python
datetime module.
"""
__author__ = "Tomi Pieviläinen <tomi.pievilainen@iki.fi>"
__license__ = "Simplified BSD"
__version__ = "2.2"
__version__ = "2.4.2"

View file

@ -1,18 +1,17 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard Python
datetime module.
This module offers a generic easter computing method for any given year, using
Western, Orthodox or Julian algorithms.
"""
__license__ = "Simplified BSD"
import datetime
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
EASTER_JULIAN = 1
EASTER_JULIAN = 1
EASTER_ORTHODOX = 2
EASTER_WESTERN = 3
EASTER_WESTERN = 3
def easter(year, method=EASTER_WESTERN):
"""
@ -68,24 +67,23 @@ def easter(year, method=EASTER_WESTERN):
e = 0
if method < 3:
# Old method
i = (19*g+15)%30
j = (y+y//4+i)%7
i = (19*g + 15) % 30
j = (y + y//4 + i) % 7
if method == 2:
# Extra dates to convert Julian to Gregorian date
e = 10
if y > 1600:
e = e+y//100-16-(y//100-16)//4
e = e + y//100 - 16 - (y//100 - 16)//4
else:
# New method
c = y//100
h = (c-c//4-(8*c+13)//25+19*g+15)%30
i = h-(h//28)*(1-(h//28)*(29//(h+1))*((21-g)//11))
j = (y+y//4+i+2-c+c//4)%7
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
j = (y + y//4 + i + 2 - c + c//4) % 7
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
# (later dates apply to method 2, although 23 May never actually occurs)
p = i-j+e
d = 1+(p+27+(p+6)//40)%31
m = 3+(p+26)//30
p = i - j + e
d = 1 + (p + 27 + (p + 6)//40) % 31
m = 3 + (p + 26)//30
return datetime.date(int(y), int(m), int(d))

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,4 @@
"""
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard Python
datetime module.
"""
__license__ = "Simplified BSD"
# -*- coding: utf-8 -*-
import datetime
import calendar
@ -13,6 +6,7 @@ from six import integer_types
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
class weekday(object):
__slots__ = ["weekday", "n"]
@ -43,25 +37,35 @@ class weekday(object):
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
class relativedelta(object):
"""
The relativedelta type is based on the specification of the excelent
work done by M.-A. Lemburg in his mx.DateTime extension. However,
notice that this type does *NOT* implement the same algorithm as
The relativedelta type is based on the specification of the excellent
work done by M.-A. Lemburg in his
`mx.DateTime <http://www.egenix.com/files/python/mxDateTime.html>`_ extension.
However, notice that this type does *NOT* implement the same algorithm as
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
There's two different ways to build a relativedelta instance. The
first one is passing it two date/datetime classes:
There are two different ways to build a relativedelta instance. The
first one is passing it two date/datetime classes::
relativedelta(datetime1, datetime2)
And the other way is to use the following keyword arguments:
The second one is passing it any number of the following keyword arguments::
relativedelta(arg1=x,arg2=y,arg3=z...)
year, month, day, hour, minute, second, microsecond:
Absolute information.
Absolute information (argument is singular); adding or subtracting a
relativedelta with absolute information does not perform an aritmetic
operation, but rather REPLACES the corresponding value in the
original datetime with the value(s) in relativedelta.
years, months, weeks, days, hours, minutes, seconds, microseconds:
Relative information, may be negative.
Relative information, may be negative (argument is plural); adding
or subtracting a relativedelta with relative information performs
the corresponding aritmetic operation on the original datetime value
with the information in the relativedelta.
weekday:
One of the weekday instances (MO, TU, etc). These instances may
@ -80,26 +84,26 @@ And the other way is to use the following keyword arguments:
Here is the behavior of operations with relativedelta:
1) Calculate the absolute year, using the 'year' argument, or the
1. Calculate the absolute year, using the 'year' argument, or the
original datetime year, if the argument is not present.
2) Add the relative 'years' argument to the absolute year.
2. Add the relative 'years' argument to the absolute year.
3) Do steps 1 and 2 for month/months.
3. Do steps 1 and 2 for month/months.
4) Calculate the absolute day, using the 'day' argument, or the
4. Calculate the absolute day, using the 'day' argument, or the
original datetime day, if the argument is not present. Then,
subtract from the day until it fits in the year and month
found after their operations.
5) Add the relative 'days' argument to the absolute day. Notice
5. Add the relative 'days' argument to the absolute day. Notice
that the 'weeks' argument is multiplied by 7 and added to
'days'.
6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
microsecond/microseconds.
7) If the 'weekday' argument is present, calculate the weekday,
7. If the 'weekday' argument is present, calculate the weekday,
with the given (wday, nth) tuple. wday is the index of the
weekday (0-6, 0=Mon), and nth is the number of weeks to add
forward or backward, depending on its signal. Notice that if
@ -114,9 +118,14 @@ Here is the behavior of operations with relativedelta:
yearday=None, nlyearday=None,
hour=None, minute=None, second=None, microsecond=None):
if dt1 and dt2:
if (not isinstance(dt1, datetime.date)) or (not isinstance(dt2, datetime.date)):
# datetime is a subclass of date. So both must be date
if not (isinstance(dt1, datetime.date) and
isinstance(dt2, datetime.date)):
raise TypeError("relativedelta only diffs datetime/date")
if not type(dt1) == type(dt2): #isinstance(dt1, type(dt2)):
# We allow two dates, or two datetimes, so we coerce them to be
# of the same type
if (isinstance(dt1, datetime.datetime) !=
isinstance(dt2, datetime.datetime)):
if not isinstance(dt1, datetime.datetime):
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
elif not isinstance(dt2, datetime.datetime):
@ -158,7 +167,7 @@ Here is the behavior of operations with relativedelta:
else:
self.years = years
self.months = months
self.days = days+weeks*7
self.days = days + weeks * 7
self.leapdays = leapdays
self.hours = hours
self.minutes = minutes
@ -185,7 +194,8 @@ Here is the behavior of operations with relativedelta:
if yearday > 59:
self.leapdays = -1
if yday:
ydayidx = [31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 366]
ydayidx = [31, 59, 90, 120, 151, 181, 212,
243, 273, 304, 334, 366]
for idx, ydays in enumerate(ydayidx):
if yday <= ydays:
self.month = idx+1
@ -225,13 +235,20 @@ Here is the behavior of operations with relativedelta:
div, mod = divmod(self.months*s, 12)
self.months = mod*s
self.years += div*s
if (self.hours or self.minutes or self.seconds or self.microseconds or
self.hour is not None or self.minute is not None or
self.second is not None or self.microsecond is not None):
if (self.hours or self.minutes or self.seconds or self.microseconds
or self.hour is not None or self.minute is not None or
self.second is not None or self.microsecond is not None):
self._has_time = 1
else:
self._has_time = 0
@property
def weeks(self):
return self.days // 7
@weeks.setter
def weeks(self, value):
self.days = self.days - (self.weeks * 7) + value*7
def _set_months(self, months):
self.months = months
if abs(self.months) > 11:
@ -244,22 +261,24 @@ Here is the behavior of operations with relativedelta:
def __add__(self, other):
if isinstance(other, relativedelta):
return relativedelta(years=other.years+self.years,
months=other.months+self.months,
days=other.days+self.days,
hours=other.hours+self.hours,
minutes=other.minutes+self.minutes,
seconds=other.seconds+self.seconds,
microseconds=other.microseconds+self.microseconds,
leapdays=other.leapdays or self.leapdays,
year=other.year or self.year,
month=other.month or self.month,
day=other.day or self.day,
weekday=other.weekday or self.weekday,
hour=other.hour or self.hour,
minute=other.minute or self.minute,
second=other.second or self.second,
microsecond=other.microsecond or self.microsecond)
return self.__class__(years=other.years+self.years,
months=other.months+self.months,
days=other.days+self.days,
hours=other.hours+self.hours,
minutes=other.minutes+self.minutes,
seconds=other.seconds+self.seconds,
microseconds=(other.microseconds +
self.microseconds),
leapdays=other.leapdays or self.leapdays,
year=other.year or self.year,
month=other.month or self.month,
day=other.day or self.day,
weekday=other.weekday or self.weekday,
hour=other.hour or self.hour,
minute=other.minute or self.minute,
second=other.second or self.second,
microsecond=(other.microsecond or
self.microsecond))
if not isinstance(other, datetime.date):
raise TypeError("unsupported type for add operation")
elif self._has_time and not isinstance(other, datetime.datetime):
@ -295,9 +314,9 @@ Here is the behavior of operations with relativedelta:
weekday, nth = self.weekday.weekday, self.weekday.n or 1
jumpdays = (abs(nth)-1)*7
if nth > 0:
jumpdays += (7-ret.weekday()+weekday)%7
jumpdays += (7-ret.weekday()+weekday) % 7
else:
jumpdays += (ret.weekday()-weekday)%7
jumpdays += (ret.weekday()-weekday) % 7
jumpdays *= -1
ret += datetime.timedelta(days=jumpdays)
return ret
@ -311,7 +330,7 @@ Here is the behavior of operations with relativedelta:
def __sub__(self, other):
if not isinstance(other, relativedelta):
raise TypeError("unsupported type for sub operation")
return relativedelta(years=self.years-other.years,
return self.__class__(years=self.years-other.years,
months=self.months-other.months,
days=self.days-other.days,
hours=self.hours-other.hours,
@ -329,7 +348,7 @@ Here is the behavior of operations with relativedelta:
microsecond=self.microsecond or other.microsecond)
def __neg__(self):
return relativedelta(years=-self.years,
return self.__class__(years=-self.years,
months=-self.months,
days=-self.days,
hours=-self.hours,
@ -363,10 +382,12 @@ Here is the behavior of operations with relativedelta:
self.minute is None and
self.second is None and
self.microsecond is None)
# Compatibility with Python 2.x
__nonzero__ = __bool__
def __mul__(self, other):
f = float(other)
return relativedelta(years=int(self.years*f),
return self.__class__(years=int(self.years*f),
months=int(self.months*f),
days=int(self.days*f),
hours=int(self.hours*f),

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,25 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard Python
datetime module.
This module offers timezone implementations subclassing the abstract
:py:`datetime.tzinfo` type. There are classes to handle tzfile format files
(usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ
environment string (in all known formats), given ranges (with help from
relative deltas), local machine timezone, fixed offset timezone, and UTC
timezone.
"""
__license__ = "Simplified BSD"
from six import string_types, PY3
import datetime
import struct
import time
import sys
import os
from six import string_types, PY3
try:
from dateutil.tzwin import tzwin, tzwinlocal
except ImportError:
tzwin = tzwinlocal = None
relativedelta = None
parser = None
rrule = None
@ -21,27 +27,26 @@ rrule = None
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"]
try:
from dateutil.tzwin import tzwin, tzwinlocal
except (ImportError, OSError):
tzwin, tzwinlocal = None, None
def tzname_in_python2(myfunc):
def tzname_in_python2(namefunc):
"""Change unicode output into bytestrings in Python 2
tzname() API changed in Python 3. It used to return bytes, but was changed
to unicode strings
"""
def inner_func(*args, **kwargs):
if PY3:
return myfunc(*args, **kwargs)
else:
return myfunc(*args, **kwargs).encode()
return inner_func
def adjust_encoding(*args, **kwargs):
name = namefunc(*args, **kwargs)
if name is not None and not PY3:
name = name.encode()
return name
return adjust_encoding
ZERO = datetime.timedelta(0)
EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
class tzutc(datetime.tzinfo):
def utcoffset(self, dt):
@ -66,6 +71,7 @@ class tzutc(datetime.tzinfo):
__reduce__ = object.__reduce__
class tzoffset(datetime.tzinfo):
def __init__(self, name, offset):
@ -96,6 +102,7 @@ class tzoffset(datetime.tzinfo):
__reduce__ = object.__reduce__
class tzlocal(datetime.tzinfo):
_std_offset = datetime.timedelta(seconds=-time.timezone)
@ -130,18 +137,18 @@ class tzlocal(datetime.tzinfo):
#
# The code above yields the following result:
#
#>>> import tz, datetime
#>>> t = tz.tzlocal()
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
#'BRDT'
#>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
#'BRST'
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
#'BRST'
#>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
#'BRDT'
#>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
#'BRDT'
# >>> import tz, datetime
# >>> t = tz.tzlocal()
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
# 'BRDT'
# >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
# 'BRST'
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
# 'BRST'
# >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
# 'BRDT'
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
# 'BRDT'
#
# Here is a more stable implementation:
#
@ -166,6 +173,7 @@ class tzlocal(datetime.tzinfo):
__reduce__ = object.__reduce__
class _ttinfo(object):
__slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
@ -205,15 +213,20 @@ class _ttinfo(object):
if name in state:
setattr(self, name, state[name])
class tzfile(datetime.tzinfo):
# http://www.twinsun.com/tz/tz-link.htm
# ftp://ftp.iana.org/tz/tz*.tar.gz
def __init__(self, fileobj):
def __init__(self, fileobj, filename=None):
file_opened_here = False
if isinstance(fileobj, string_types):
self._filename = fileobj
fileobj = open(fileobj, 'rb')
file_opened_here = True
elif filename is not None:
self._filename = filename
elif hasattr(fileobj, "name"):
self._filename = fileobj.name
else:
@ -228,125 +241,128 @@ class tzfile(datetime.tzinfo):
# six four-byte values of type long, written in a
# ``standard'' byte order (the high-order byte
# of the value is written first).
try:
if fileobj.read(4).decode() != "TZif":
raise ValueError("magic not found")
if fileobj.read(4).decode() != "TZif":
raise ValueError("magic not found")
fileobj.read(16)
fileobj.read(16)
(
# The number of UTC/local indicators stored in the file.
ttisgmtcnt,
(
# The number of UTC/local indicators stored in the file.
ttisgmtcnt,
# The number of standard/wall indicators stored in the file.
ttisstdcnt,
# The number of standard/wall indicators stored in the file.
ttisstdcnt,
# The number of leap seconds for which data is
# stored in the file.
leapcnt,
# The number of leap seconds for which data is
# stored in the file.
leapcnt,
# The number of "transition times" for which data
# is stored in the file.
timecnt,
# The number of "transition times" for which data
# is stored in the file.
timecnt,
# The number of "local time types" for which data
# is stored in the file (must not be zero).
typecnt,
# The number of "local time types" for which data
# is stored in the file (must not be zero).
typecnt,
# The number of characters of "time zone
# abbreviation strings" stored in the file.
charcnt,
# The number of characters of "time zone
# abbreviation strings" stored in the file.
charcnt,
) = struct.unpack(">6l", fileobj.read(24))
) = struct.unpack(">6l", fileobj.read(24))
# The above header is followed by tzh_timecnt four-byte
# values of type long, sorted in ascending order.
# These values are written in ``standard'' byte order.
# Each is used as a transition time (as returned by
# time(2)) at which the rules for computing local time
# change.
# The above header is followed by tzh_timecnt four-byte
# values of type long, sorted in ascending order.
# These values are written in ``standard'' byte order.
# Each is used as a transition time (as returned by
# time(2)) at which the rules for computing local time
# change.
if timecnt:
self._trans_list = struct.unpack(">%dl" % timecnt,
fileobj.read(timecnt*4))
else:
self._trans_list = []
if timecnt:
self._trans_list = struct.unpack(">%dl" % timecnt,
fileobj.read(timecnt*4))
else:
self._trans_list = []
# Next come tzh_timecnt one-byte values of type unsigned
# char; each one tells which of the different types of
# ``local time'' types described in the file is associated
# with the same-indexed transition time. These values
# serve as indices into an array of ttinfo structures that
# appears next in the file.
# Next come tzh_timecnt one-byte values of type unsigned
# char; each one tells which of the different types of
# ``local time'' types described in the file is associated
# with the same-indexed transition time. These values
# serve as indices into an array of ttinfo structures that
# appears next in the file.
if timecnt:
self._trans_idx = struct.unpack(">%dB" % timecnt,
fileobj.read(timecnt))
else:
self._trans_idx = []
if timecnt:
self._trans_idx = struct.unpack(">%dB" % timecnt,
fileobj.read(timecnt))
else:
self._trans_idx = []
# Each ttinfo structure is written as a four-byte value
# for tt_gmtoff of type long, in a standard byte
# order, followed by a one-byte value for tt_isdst
# and a one-byte value for tt_abbrind. In each
# structure, tt_gmtoff gives the number of
# seconds to be added to UTC, tt_isdst tells whether
# tm_isdst should be set by localtime(3), and
# tt_abbrind serves as an index into the array of
# time zone abbreviation characters that follow the
# ttinfo structure(s) in the file.
# Each ttinfo structure is written as a four-byte value
# for tt_gmtoff of type long, in a standard byte
# order, followed by a one-byte value for tt_isdst
# and a one-byte value for tt_abbrind. In each
# structure, tt_gmtoff gives the number of
# seconds to be added to UTC, tt_isdst tells whether
# tm_isdst should be set by localtime(3), and
# tt_abbrind serves as an index into the array of
# time zone abbreviation characters that follow the
# ttinfo structure(s) in the file.
ttinfo = []
ttinfo = []
for i in range(typecnt):
ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
for i in range(typecnt):
ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
abbr = fileobj.read(charcnt).decode()
abbr = fileobj.read(charcnt).decode()
# Then there are tzh_leapcnt pairs of four-byte
# values, written in standard byte order; the
# first value of each pair gives the time (as
# returned by time(2)) at which a leap second
# occurs; the second gives the total number of
# leap seconds to be applied after the given time.
# The pairs of values are sorted in ascending order
# by time.
# Then there are tzh_leapcnt pairs of four-byte
# values, written in standard byte order; the
# first value of each pair gives the time (as
# returned by time(2)) at which a leap second
# occurs; the second gives the total number of
# leap seconds to be applied after the given time.
# The pairs of values are sorted in ascending order
# by time.
# Not used, for now
# if leapcnt:
# leap = struct.unpack(">%dl" % (leapcnt*2),
# fileobj.read(leapcnt*8))
# Not used, for now
if leapcnt:
leap = struct.unpack(">%dl" % (leapcnt*2),
fileobj.read(leapcnt*8))
# Then there are tzh_ttisstdcnt standard/wall
# indicators, each stored as a one-byte value;
# they tell whether the transition times associated
# with local time types were specified as standard
# time or wall clock time, and are used when
# a time zone file is used in handling POSIX-style
# time zone environment variables.
# Then there are tzh_ttisstdcnt standard/wall
# indicators, each stored as a one-byte value;
# they tell whether the transition times associated
# with local time types were specified as standard
# time or wall clock time, and are used when
# a time zone file is used in handling POSIX-style
# time zone environment variables.
if ttisstdcnt:
isstd = struct.unpack(">%db" % ttisstdcnt,
fileobj.read(ttisstdcnt))
if ttisstdcnt:
isstd = struct.unpack(">%db" % ttisstdcnt,
fileobj.read(ttisstdcnt))
# Finally, there are tzh_ttisgmtcnt UTC/local
# indicators, each stored as a one-byte value;
# they tell whether the transition times associated
# with local time types were specified as UTC or
# local time, and are used when a time zone file
# is used in handling POSIX-style time zone envi-
# ronment variables.
# Finally, there are tzh_ttisgmtcnt UTC/local
# indicators, each stored as a one-byte value;
# they tell whether the transition times associated
# with local time types were specified as UTC or
# local time, and are used when a time zone file
# is used in handling POSIX-style time zone envi-
# ronment variables.
if ttisgmtcnt:
isgmt = struct.unpack(">%db" % ttisgmtcnt,
fileobj.read(ttisgmtcnt))
if ttisgmtcnt:
isgmt = struct.unpack(">%db" % ttisgmtcnt,
fileobj.read(ttisgmtcnt))
# ** Everything has been read **
# ** Everything has been read **
finally:
if file_opened_here:
fileobj.close()
# Build ttinfo list
self._ttinfo_list = []
for i in range(typecnt):
gmtoff, isdst, abbrind = ttinfo[i]
gmtoff, isdst, abbrind = ttinfo[i]
# Round to full-minutes if that's not the case. Python's
# datetime doesn't accept sub-minute timezones. Check
# http://python.org/sf/1447945 for some information.
@ -481,7 +497,6 @@ class tzfile(datetime.tzinfo):
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
@ -490,8 +505,8 @@ class tzfile(datetime.tzinfo):
raise ValueError("Unpickable %s class" % self.__class__.__name__)
return (self.__class__, (self._filename,))
class tzrange(datetime.tzinfo):
class tzrange(datetime.tzinfo):
def __init__(self, stdabbr, stdoffset=None,
dstabbr=None, dstoffset=None,
start=None, end=None):
@ -512,12 +527,12 @@ class tzrange(datetime.tzinfo):
self._dst_offset = ZERO
if dstabbr and start is None:
self._start_delta = relativedelta.relativedelta(
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
else:
self._start_delta = start
if dstabbr and end is None:
self._end_delta = relativedelta.relativedelta(
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
else:
self._end_delta = end
@ -570,6 +585,7 @@ class tzrange(datetime.tzinfo):
__reduce__ = object.__reduce__
class tzstr(tzrange):
def __init__(self, s):
@ -645,9 +661,10 @@ class tzstr(tzrange):
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, repr(self._s))
class _tzicalvtzcomp(object):
def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
tzname=None, rrule=None):
tzname=None, rrule=None):
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
@ -655,6 +672,7 @@ class _tzicalvtzcomp(object):
self.tzname = tzname
self.rrule = rrule
class _tzicalvtz(datetime.tzinfo):
def __init__(self, tzid, comps=[]):
self._tzid = tzid
@ -718,6 +736,7 @@ class _tzicalvtz(datetime.tzinfo):
__reduce__ = object.__reduce__
class tzical(object):
def __init__(self, fileobj):
global rrule
@ -726,7 +745,8 @@ class tzical(object):
if isinstance(fileobj, string_types):
self._s = fileobj
fileobj = open(fileobj, 'r') # ical should be encoded in UTF-8 with CRLF
# ical should be encoded in UTF-8 with CRLF
fileobj = open(fileobj, 'r')
elif hasattr(fileobj, "name"):
self._s = fileobj.name
else:
@ -754,7 +774,7 @@ class tzical(object):
if not s:
raise ValueError("empty offset")
if s[0] in ('+', '-'):
signal = (-1, +1)[s[0]=='+']
signal = (-1, +1)[s[0] == '+']
s = s[1:]
else:
signal = +1
@ -815,7 +835,8 @@ class tzical(object):
if not tzid:
raise ValueError("mandatory TZID not found")
if not comps:
raise ValueError("at least one component is needed")
raise ValueError(
"at least one component is needed")
# Process vtimezone
self._vtz[tzid] = _tzicalvtz(tzid, comps)
invtz = False
@ -823,9 +844,11 @@ class tzical(object):
if not founddtstart:
raise ValueError("mandatory DTSTART not found")
if tzoffsetfrom is None:
raise ValueError("mandatory TZOFFSETFROM not found")
raise ValueError(
"mandatory TZOFFSETFROM not found")
if tzoffsetto is None:
raise ValueError("mandatory TZOFFSETFROM not found")
raise ValueError(
"mandatory TZOFFSETFROM not found")
# Process component
rr = None
if rrulelines:
@ -848,15 +871,18 @@ class tzical(object):
rrulelines.append(line)
elif name == "TZOFFSETFROM":
if parms:
raise ValueError("unsupported %s parm: %s "%(name, parms[0]))
raise ValueError(
"unsupported %s parm: %s " % (name, parms[0]))
tzoffsetfrom = self._parse_offset(value)
elif name == "TZOFFSETTO":
if parms:
raise ValueError("unsupported TZOFFSETTO parm: "+parms[0])
raise ValueError(
"unsupported TZOFFSETTO parm: "+parms[0])
tzoffsetto = self._parse_offset(value)
elif name == "TZNAME":
if parms:
raise ValueError("unsupported TZNAME parm: "+parms[0])
raise ValueError(
"unsupported TZNAME parm: "+parms[0])
tzname = value
elif name == "COMMENT":
pass
@ -865,7 +891,8 @@ class tzical(object):
else:
if name == "TZID":
if parms:
raise ValueError("unsupported TZID parm: "+parms[0])
raise ValueError(
"unsupported TZID parm: "+parms[0])
tzid = value
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
pass
@ -886,6 +913,7 @@ else:
TZFILES = []
TZPATHS = []
def gettz(name=None):
tz = None
if not name:
@ -933,11 +961,11 @@ def gettz(name=None):
pass
else:
tz = None
if tzwin:
if tzwin is not None:
try:
tz = tzwin(name)
except OSError:
pass
except WindowsError:
tz = None
if not tz:
from dateutil.zoneinfo import gettz
tz = gettz(name)

View file

@ -1,8 +1,8 @@
# This code was originally contributed by Jeffrey Harris.
import datetime
import struct
import winreg
from six.moves import winreg
__all__ = ["tzwin", "tzwinlocal"]
@ -12,8 +12,8 @@ TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
def _settzkeyname():
global TZKEYNAME
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
try:
winreg.OpenKey(handle, TZKEYNAMENT).Close()
@ -21,8 +21,10 @@ def _settzkeyname():
except WindowsError:
TZKEYNAME = TZKEYNAME9X
handle.Close()
return TZKEYNAME
TZKEYNAME = _settzkeyname()
_settzkeyname()
class tzwinbase(datetime.tzinfo):
"""tzinfo class based on win32's timezones available in the registry."""
@ -61,6 +63,9 @@ class tzwinbase(datetime.tzinfo):
return self._display
def _isdst(self, dt):
if not self._dstmonth:
# dstmonth == 0 signals the zone has no daylight saving time
return False
dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek,
self._dsthour, self._dstminute,
self._dstweeknumber)
@ -78,11 +83,11 @@ class tzwin(tzwinbase):
def __init__(self, name):
self._name = name
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
tzkey = winreg.OpenKey(handle, "%s\%s" % (TZKEYNAME, name))
keydict = valuestodict(tzkey)
tzkey.Close()
handle.Close()
# multiple contexts only possible in 2.7 and 3.1, we still support 2.6
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
with winreg.OpenKey(handle,
"%s\%s" % (TZKEYNAME, name)) as tzkey:
keydict = valuestodict(tzkey)
self._stdname = keydict["Std"].encode("iso-8859-1")
self._dstname = keydict["Dlt"].encode("iso-8859-1")
@ -91,18 +96,20 @@ class tzwin(tzwinbase):
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
tup = struct.unpack("=3l16h", keydict["TZI"])
self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1
self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
(self._stdmonth,
self._stddayofweek, # Sunday = 0
self._stdweeknumber, # Last = 5
self._stddayofweek, # Sunday = 0
self._stdweeknumber, # Last = 5
self._stdhour,
self._stdminute) = tup[4:9]
(self._dstmonth,
self._dstdayofweek, # Sunday = 0
self._dstweeknumber, # Last = 5
self._dstdayofweek, # Sunday = 0
self._dstweeknumber, # Last = 5
self._dsthour,
self._dstminute) = tup[12:17]
@ -117,58 +124,56 @@ class tzwinlocal(tzwinbase):
def __init__(self):
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
tzlocalkey = winreg.OpenKey(handle, TZLOCALKEYNAME)
keydict = valuestodict(tzlocalkey)
tzlocalkey.Close()
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
keydict = valuestodict(tzlocalkey)
self._stdname = keydict["StandardName"].encode("iso-8859-1")
self._dstname = keydict["DaylightName"].encode("iso-8859-1")
self._stdname = keydict["StandardName"].encode("iso-8859-1")
self._dstname = keydict["DaylightName"].encode("iso-8859-1")
try:
tzkey = winreg.OpenKey(handle, "%s\%s"%(TZKEYNAME, self._stdname))
_keydict = valuestodict(tzkey)
self._display = _keydict["Display"]
tzkey.Close()
except OSError:
self._display = None
handle.Close()
try:
with winreg.OpenKey(
handle, "%s\%s" % (TZKEYNAME, self._stdname)) as tzkey:
_keydict = valuestodict(tzkey)
self._display = _keydict["Display"]
except OSError:
self._display = None
self._stdoffset = -keydict["Bias"]-keydict["StandardBias"]
self._dstoffset = self._stdoffset-keydict["DaylightBias"]
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
tup = struct.unpack("=8h", keydict["StandardStart"])
(self._stdmonth,
self._stddayofweek, # Sunday = 0
self._stdweeknumber, # Last = 5
self._stddayofweek, # Sunday = 0
self._stdweeknumber, # Last = 5
self._stdhour,
self._stdminute) = tup[1:6]
tup = struct.unpack("=8h", keydict["DaylightStart"])
(self._dstmonth,
self._dstdayofweek, # Sunday = 0
self._dstweeknumber, # Last = 5
self._dstdayofweek, # Sunday = 0
self._dstweeknumber, # Last = 5
self._dsthour,
self._dstminute) = tup[1:6]
def __reduce__(self):
return (self.__class__, ())
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
"""dayofweek == 0 means Sunday, whichweek 5 means last instance"""
first = datetime.datetime(year, month, 1, hour, minute)
weekdayone = first.replace(day=((dayofweek-first.isoweekday())%7+1))
weekdayone = first.replace(day=((dayofweek-first.isoweekday()) % 7+1))
for n in range(whichweek):
dt = weekdayone+(whichweek-n)*ONEWEEK
if dt.month == month:
return dt
def valuestodict(key):
"""Convert a registry key's values to a dictionary."""
dict = {}

View file

@ -1,109 +1,135 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2003-2005 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard Python
datetime module.
"""
import logging
import os
from subprocess import call
import warnings
import tempfile
import shutil
import json
from subprocess import check_call
from tarfile import TarFile
from pkgutil import get_data
from io import BytesIO
from contextlib import closing
from dateutil.tz import tzfile
__author__ = "Tomi Pieviläinen <tomi.pievilainen@iki.fi>"
__license__ = "Simplified BSD"
__all__ = ["gettz", "gettz_db_metadata", "rebuild"]
__all__ = ["setcachesize", "gettz", "rebuild"]
_ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
_METADATA_FN = 'METADATA'
# python2.6 compatability. Note that TarFile.__exit__ != TarFile.close, but
# it's close enough for python2.6
_tar_open = TarFile.open
if not hasattr(TarFile, '__exit__'):
def _tar_open(*args, **kwargs):
return closing(TarFile.open(*args, **kwargs))
CACHE = []
CACHESIZE = 10
class tzfile(tzfile):
def __reduce__(self):
return (gettz, (self._filename,))
def getzoneinfofile():
filenames = sorted(os.listdir(os.path.join(os.path.dirname(__file__))))
filenames.reverse()
for entry in filenames:
if entry.startswith("zoneinfo") and ".tar." in entry:
return os.path.join(os.path.dirname(__file__), entry)
return None
ZONEINFOFILE = getzoneinfofile()
def getzoneinfofile_stream():
try:
return BytesIO(get_data(__name__, _ZONEFILENAME))
except IOError as e: # TODO switch to FileNotFoundError?
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
return None
del getzoneinfofile
def setcachesize(size):
global CACHESIZE, CACHE
CACHESIZE = size
del CACHE[size:]
class ZoneInfoFile(object):
def __init__(self, zonefile_stream=None):
if zonefile_stream is not None:
with _tar_open(fileobj=zonefile_stream, mode='r') as tf:
# dict comprehension does not work on python2.6
# TODO: get back to the nicer syntax when we ditch python2.6
# self.zones = {zf.name: tzfile(tf.extractfile(zf),
# filename = zf.name)
# for zf in tf.getmembers() if zf.isfile()}
self.zones = dict((zf.name, tzfile(tf.extractfile(zf),
filename=zf.name))
for zf in tf.getmembers()
if zf.isfile() and zf.name != _METADATA_FN)
# deal with links: They'll point to their parent object. Less
# waste of memory
# links = {zl.name: self.zones[zl.linkname]
# for zl in tf.getmembers() if zl.islnk() or zl.issym()}
links = dict((zl.name, self.zones[zl.linkname])
for zl in tf.getmembers() if
zl.islnk() or zl.issym())
self.zones.update(links)
try:
metadata_json = tf.extractfile(tf.getmember(_METADATA_FN))
metadata_str = metadata_json.read().decode('UTF-8')
self.metadata = json.loads(metadata_str)
except KeyError:
# no metadata in tar file
self.metadata = None
else:
self.zones = dict()
self.metadata = None
# The current API has gettz as a module function, although in fact it taps into
# a stateful class. So as a workaround for now, without changing the API, we
# will create a new "global" class instance the first time a user requests a
# timezone. Ugly, but adheres to the api.
#
# TODO: deprecate this.
_CLASS_ZONE_INSTANCE = list()
def gettz(name):
tzinfo = None
if ZONEINFOFILE:
for cachedname, tzinfo in CACHE:
if cachedname == name:
break
else:
tf = TarFile.open(ZONEINFOFILE)
try:
zonefile = tf.extractfile(name)
except KeyError:
tzinfo = None
else:
tzinfo = tzfile(zonefile)
tf.close()
CACHE.insert(0, (name, tzinfo))
del CACHE[CACHESIZE:]
return tzinfo
if len(_CLASS_ZONE_INSTANCE) == 0:
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
def rebuild(filename, tag=None, format="gz"):
def gettz_db_metadata():
""" Get the zonefile metadata
See `zonefile_metadata`_
:returns: A dictionary with the database metadata
"""
if len(_CLASS_ZONE_INSTANCE) == 0:
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
return _CLASS_ZONE_INSTANCE[0].metadata
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
filename is the timezone tarball from ftp.iana.org/tz.
"""
import tempfile, shutil
tmpdir = tempfile.mkdtemp()
zonedir = os.path.join(tmpdir, "zoneinfo")
moduledir = os.path.dirname(__file__)
if tag: tag = "-"+tag
targetname = "zoneinfo%s.tar.%s" % (tag, format)
try:
tf = TarFile.open(filename)
# The "backwards" zone file contains links to other files, so must be
# processed as last
for name in sorted(tf.getnames(),
key=lambda k: k != "backward" and k or "z"):
if not (name.endswith(".sh") or
name.endswith(".tab") or
name == "leapseconds"):
with _tar_open(filename) as tf:
for name in zonegroups:
tf.extract(name, tmpdir)
filepath = os.path.join(tmpdir, name)
try:
# zic will return errors for nontz files in the package
# such as the Makefile or README, so check_call cannot
# be used (or at least extra checks would be needed)
call(["zic", "-d", zonedir, filepath])
except OSError as e:
if e.errno == 2:
logging.error(
"Could not find zic. Perhaps you need to install "
"libc-bin or some other package that provides it, "
"or it's not in your PATH?")
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
try:
check_call(["zic", "-d", zonedir] + filepaths)
except OSError as e:
if e.errno == 2:
logging.error(
"Could not find zic. Perhaps you need to install "
"libc-bin or some other package that provides it, "
"or it's not in your PATH?")
raise
tf.close()
target = os.path.join(moduledir, targetname)
for entry in os.listdir(moduledir):
if entry.startswith("zoneinfo") and ".tar." in entry:
os.unlink(os.path.join(moduledir, entry))
tf = TarFile.open(target, "w:%s" % format)
for entry in os.listdir(zonedir):
entrypath = os.path.join(zonedir, entry)
tf.add(entrypath, entry)
tf.close()
# write metadata file
with open(os.path.join(zonedir, _METADATA_FN), 'w') as f:
json.dump(metadata, f, indent=4, sort_keys=True)
target = os.path.join(moduledir, _ZONEFILENAME)
with _tar_open(target, "w:%s" % format) as tf:
for entry in os.listdir(zonedir):
entrypath = os.path.join(zonedir, entry)
tf.add(entrypath, entry)
finally:
shutil.rmtree(tmpdir)

View file

@ -1,44 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""
"""
__module_id__ = "$Id$"
#
# Import system modules
#
#
# Import local modules
#
from cache import Cache
#
# Module
#

View file

@ -1,204 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""
"""
__module_id__ = "$Id$"
#
# Import system modules
#
from feedparser import feedparser
import logging
import time
#
# Import local modules
#
#
# Module
#
logger = logging.getLogger('feedcache.cache')
class Cache:
"""A class to wrap Mark Pilgrim's Universal Feed Parser module
(http://www.feedparser.org) so that parameters can be used to
cache the feed results locally instead of fetching the feed every
time it is requested. Uses both etag and modified times for
caching.
"""
def __init__(self, storage, timeToLiveSeconds=300, userAgent='feedcache'):
"""
Arguments:
storage -- Backing store for the cache. It should follow
the dictionary API, with URLs used as keys. It should
persist data.
timeToLiveSeconds=300 -- The length of time content should
live in the cache before an update is attempted.
userAgent='feedcache' -- User agent string to be used when
fetching feed contents.
"""
self.storage = storage
self.time_to_live = timeToLiveSeconds
self.user_agent = userAgent
return
def purge(self, olderThanSeconds):
"""Remove cached data from the storage if the data is older than the
date given. If olderThanSeconds is None, the entire cache is purged.
"""
if olderThanSeconds is None:
logger.debug('purging the entire cache')
for key in self.storage.keys():
del self.storage[key]
else:
now = time.time()
# Iterate over the keys and load each item one at a time
# to avoid having the entire cache loaded into memory
# at one time.
for url in self.storage.keys():
(cached_time, cached_data) = self.storage[url]
age = now - cached_time
if age >= olderThanSeconds:
logger.debug('removing %s with age %d', url, age)
del self.storage[url]
return
def fetch(self, url, force_update=False, offline=False, request_headers=None):
"""Return the feed at url.
url - The URL of the feed.
force_update=False - When True, update the cache whether the
current contents have
exceeded their time-to-live
or not.
offline=False - When True, only return data from the local
cache and never access the remote
URL.
If there is data for that feed in the cache already, check
the expiration date before accessing the server. If the
cached data has not expired, return it without accessing the
server.
In cases where the server is accessed, check for updates
before deciding what to return. If the server reports a
status of 304, the previously cached content is returned.
The cache is only updated if the server returns a status of
200, to avoid holding redirected data in the cache.
"""
logger.debug('url="%s"' % url)
# Convert the URL to a value we can use
# as a key for the storage backend.
key = url
if isinstance(key, unicode):
key = key.encode('utf-8')
modified = None
etag = None
now = time.time()
cached_time, cached_content = self.storage.get(key, (None, None))
# Offline mode support (no networked requests)
# so return whatever we found in the storage.
# If there is nothing in the storage, we'll be returning None.
if offline:
logger.debug('offline mode')
return cached_content
# Does the storage contain a version of the data
# which is older than the time-to-live?
logger.debug('cache modified time: %s' % str(cached_time))
if cached_time is not None and not force_update:
if self.time_to_live:
age = now - cached_time
if age <= self.time_to_live:
logger.debug('cache contents still valid')
return cached_content
else:
logger.debug('cache contents older than TTL')
else:
logger.debug('no TTL value')
# The cache is out of date, but we have
# something. Try to use the etag and modified_time
# values from the cached content.
etag = cached_content.get('etag')
modified = cached_content.get('modified')
logger.debug('cached etag=%s' % etag)
logger.debug('cached modified=%s' % str(modified))
else:
logger.debug('nothing in the cache, or forcing update')
# We know we need to fetch, so go ahead and do it.
logger.debug('fetching...')
parsed_result = feedparser.parse(url,
agent=self.user_agent,
modified=modified,
etag=etag,
request_headers=request_headers)
status = parsed_result.get('status', None)
logger.debug('HTTP status=%s' % status)
if status == 304:
# No new data, based on the etag or modified values.
# We need to update the modified time in the
# storage, though, so we know that what we have
# stored is up to date.
self.storage[key] = (now, cached_content)
# Return the data from the cache, since
# the parsed data will be empty.
parsed_result = cached_content
elif status == 200:
# There is new content, so store it unless there was an error.
error = parsed_result.get('bozo_exception')
if not error:
logger.debug('Updating stored data for %s' % url)
self.storage[key] = (now, parsed_result)
else:
logger.warning('Not storing data with exception: %s',
error)
else:
logger.warning('Not updating cache with HTTP status %s', status)
return parsed_result

View file

@ -1,69 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
from __future__ import with_statement
"""Lock wrapper for cache storage which do not permit multi-threaded access.
"""
__module_id__ = "$Id$"
#
# Import system modules
#
import threading
#
# Import local modules
#
#
# Module
#
class CacheStorageLock:
"""Lock wrapper for cache storage which do not permit multi-threaded access.
"""
def __init__(self, shelf):
self.lock = threading.Lock()
self.shelf = shelf
return
def __getitem__(self, key):
with self.lock:
return self.shelf[key]
def get(self, key, default=None):
with self.lock:
try:
return self.shelf[key]
except KeyError:
return default
def __setitem__(self, key, value):
with self.lock:
self.shelf[key] = value

View file

@ -1,63 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""Example use of feedcache.Cache.
"""
__module_id__ = "$Id$"
#
# Import system modules
#
import sys
import shelve
#
# Import local modules
#
import cache
#
# Module
#
def main(urls=[]):
print 'Saving feed data to ./.feedcache'
storage = shelve.open('.feedcache')
try:
fc = cache.Cache(storage)
for url in urls:
parsed_data = fc.fetch(url)
print parsed_data.feed.title
for entry in parsed_data.entries:
print '\t', entry.title
finally:
storage.close()
return
if __name__ == '__main__':
main(sys.argv[1:])

View file

@ -1,144 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""Example use of feedcache.Cache combined with threads.
"""
__module_id__ = "$Id$"
#
# Import system modules
#
import Queue
import sys
import shove
import threading
#
# Import local modules
#
import cache
#
# Module
#
MAX_THREADS=5
OUTPUT_DIR='/tmp/feedcache_example'
def main(urls=[]):
if not urls:
print 'Specify the URLs to a few RSS or Atom feeds on the command line.'
return
# Decide how many threads to start
num_threads = min(len(urls), MAX_THREADS)
# Add the URLs to a queue
url_queue = Queue.Queue()
for url in urls:
url_queue.put(url)
# Add poison pills to the url queue to cause
# the worker threads to break out of their loops
for i in range(num_threads):
url_queue.put(None)
# Track the entries in the feeds being fetched
entry_queue = Queue.Queue()
print 'Saving feed data to', OUTPUT_DIR
storage = shove.Shove('file://' + OUTPUT_DIR)
try:
# Start a few worker threads
worker_threads = []
for i in range(num_threads):
t = threading.Thread(target=fetch_urls,
args=(storage, url_queue, entry_queue,))
worker_threads.append(t)
t.setDaemon(True)
t.start()
# Start a thread to print the results
printer_thread = threading.Thread(target=print_entries, args=(entry_queue,))
printer_thread.setDaemon(True)
printer_thread.start()
# Wait for all of the URLs to be processed
url_queue.join()
# Wait for the worker threads to finish
for t in worker_threads:
t.join()
# Poison the print thread and wait for it to exit
entry_queue.put((None,None))
entry_queue.join()
printer_thread.join()
finally:
storage.close()
return
def fetch_urls(storage, input_queue, output_queue):
"""Thread target for fetching feed data.
"""
c = cache.Cache(storage)
while True:
next_url = input_queue.get()
if next_url is None: # None causes thread to exit
input_queue.task_done()
break
feed_data = c.fetch(next_url)
for entry in feed_data.entries:
output_queue.put( (feed_data.feed, entry) )
input_queue.task_done()
return
def print_entries(input_queue):
"""Thread target for printing the contents of the feeds.
"""
while True:
feed, entry = input_queue.get()
if feed is None: # None causes thread to exist
input_queue.task_done()
break
print '%s: %s' % (feed.title, entry.title)
input_queue.task_done()
return
if __name__ == '__main__':
main(sys.argv[1:])

View file

@ -1,323 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""Unittests for feedcache.cache
"""
__module_id__ = "$Id$"
import logging
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(levelname)-8s %(name)s %(message)s',
)
logger = logging.getLogger('feedcache.test_cache')
#
# Import system modules
#
import copy
import time
import unittest
import UserDict
#
# Import local modules
#
import cache
from test_server import HTTPTestBase, TestHTTPServer
#
# Module
#
class CacheTestBase(HTTPTestBase):
CACHE_TTL = 30
def setUp(self):
HTTPTestBase.setUp(self)
self.storage = self.getStorage()
self.cache = cache.Cache(self.storage,
timeToLiveSeconds=self.CACHE_TTL,
userAgent='feedcache.test',
)
return
def getStorage(self):
"Return a cache storage for the test."
return {}
class CacheTest(CacheTestBase):
CACHE_TTL = 30
def getServer(self):
"These tests do not want to use the ETag or If-Modified-Since headers"
return TestHTTPServer(applyModifiedHeaders=False)
def testRetrieveNotInCache(self):
# Retrieve data not already in the cache.
feed_data = self.cache.fetch(self.TEST_URL)
self.failUnless(feed_data)
self.failUnlessEqual(feed_data.feed.title, 'CacheTest test data')
return
def testRetrieveIsInCache(self):
# Retrieve data which is alread in the cache,
# and verify that the second copy is identitical
# to the first.
# First fetch
feed_data = self.cache.fetch(self.TEST_URL)
# Second fetch
feed_data2 = self.cache.fetch(self.TEST_URL)
# Since it is the in-memory storage, we should have the
# exact same object.
self.failUnless(feed_data is feed_data2)
return
def testExpireDataInCache(self):
# Retrieve data which is in the cache but which
# has expired and verify that the second copy
# is different from the first.
# First fetch
feed_data = self.cache.fetch(self.TEST_URL)
# Change the timeout and sleep to move the clock
self.cache.time_to_live = 0
time.sleep(1)
# Second fetch
feed_data2 = self.cache.fetch(self.TEST_URL)
# Since we reparsed, the cache response should be different.
self.failIf(feed_data is feed_data2)
return
def testForceUpdate(self):
# Force cache to retrieve data which is alread in the cache,
# and verify that the new data is different.
# Pre-populate the storage with bad data
self.cache.storage[self.TEST_URL] = (time.time() + 100, self.id())
# Fetch the data
feed_data = self.cache.fetch(self.TEST_URL, force_update=True)
self.failIfEqual(feed_data, self.id())
return
def testOfflineMode(self):
# Retrieve data which is alread in the cache,
# whether it is expired or not.
# Pre-populate the storage with data
self.cache.storage[self.TEST_URL] = (0, self.id())
# Fetch it
feed_data = self.cache.fetch(self.TEST_URL, offline=True)
self.failUnlessEqual(feed_data, self.id())
return
def testUnicodeURL(self):
# Pass in a URL which is unicode
url = unicode(self.TEST_URL)
feed_data = self.cache.fetch(url)
storage = self.cache.storage
key = unicode(self.TEST_URL).encode('UTF-8')
# Verify that the storage has a key
self.failUnless(key in storage)
# Now pull the data from the storage directly
storage_timeout, storage_data = self.cache.storage.get(key)
self.failUnlessEqual(feed_data, storage_data)
return
class SingleWriteMemoryStorage(UserDict.UserDict):
"""Cache storage which only allows the cache value
for a URL to be updated one time.
"""
def __setitem__(self, url, data):
if url in self.keys():
modified, existing = self[url]
# Allow the modified time to change,
# but not the feed content.
if data[1] != existing:
raise AssertionError('Trying to update cache for %s to %s' \
% (url, data))
UserDict.UserDict.__setitem__(self, url, data)
return
class CacheConditionalGETTest(CacheTestBase):
CACHE_TTL = 0
def getStorage(self):
return SingleWriteMemoryStorage()
def testFetchOnceForEtag(self):
# Fetch data which has a valid ETag value, and verify
# that while we hit the server twice the response
# codes cause us to use the same data.
# First fetch populates the cache
response1 = self.cache.fetch(self.TEST_URL)
self.failUnlessEqual(response1.feed.title, 'CacheTest test data')
# Remove the modified setting from the cache so we know
# the next time we check the etag will be used
# to check for updates. Since we are using an in-memory
# cache, modifying response1 updates the cache storage
# directly.
response1['modified'] = None
# This should result in a 304 status, and no data from
# the server. That means the cache won't try to
# update the storage, so our SingleWriteMemoryStorage
# should not raise and we should have the same
# response object.
response2 = self.cache.fetch(self.TEST_URL)
self.failUnless(response1 is response2)
# Should have hit the server twice
self.failUnlessEqual(self.server.getNumRequests(), 2)
return
def testFetchOnceForModifiedTime(self):
# Fetch data which has a valid Last-Modified value, and verify
# that while we hit the server twice the response
# codes cause us to use the same data.
# First fetch populates the cache
response1 = self.cache.fetch(self.TEST_URL)
self.failUnlessEqual(response1.feed.title, 'CacheTest test data')
# Remove the etag setting from the cache so we know
# the next time we check the modified time will be used
# to check for updates. Since we are using an in-memory
# cache, modifying response1 updates the cache storage
# directly.
response1['etag'] = None
# This should result in a 304 status, and no data from
# the server. That means the cache won't try to
# update the storage, so our SingleWriteMemoryStorage
# should not raise and we should have the same
# response object.
response2 = self.cache.fetch(self.TEST_URL)
self.failUnless(response1 is response2)
# Should have hit the server twice
self.failUnlessEqual(self.server.getNumRequests(), 2)
return
class CacheRedirectHandlingTest(CacheTestBase):
def _test(self, response):
# Set up the server to redirect requests,
# then verify that the cache is not updated
# for the original or new URL and that the
# redirect status is fed back to us with
# the fetched data.
self.server.setResponse(response, '/redirected')
response1 = self.cache.fetch(self.TEST_URL)
# The response should include the status code we set
self.failUnlessEqual(response1.get('status'), response)
# The response should include the new URL, too
self.failUnlessEqual(response1.href, self.TEST_URL + 'redirected')
# The response should not have been cached under either URL
self.failIf(self.TEST_URL in self.storage)
self.failIf(self.TEST_URL + 'redirected' in self.storage)
return
def test301(self):
self._test(301)
def test302(self):
self._test(302)
def test303(self):
self._test(303)
def test307(self):
self._test(307)
class CachePurgeTest(CacheTestBase):
def testPurgeAll(self):
# Remove everything from the cache
self.cache.fetch(self.TEST_URL)
self.failUnless(self.storage.keys(),
'Have no data in the cache storage')
self.cache.purge(None)
self.failIf(self.storage.keys(),
'Still have data in the cache storage')
return
def testPurgeByAge(self):
# Remove old content from the cache
self.cache.fetch(self.TEST_URL)
self.failUnless(self.storage.keys(),
'have no data in the cache storage')
time.sleep(1)
remains = (time.time(), copy.deepcopy(self.storage[self.TEST_URL][1]))
self.storage['http://this.should.remain/'] = remains
self.cache.purge(1)
self.failUnlessEqual(self.storage.keys(),
['http://this.should.remain/'])
return
if __name__ == '__main__':
unittest.main()

View file

@ -1,90 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""Tests for shelflock.
"""
__module_id__ = "$Id$"
#
# Import system modules
#
import os
import shelve
import tempfile
import threading
import unittest
#
# Import local modules
#
from cache import Cache
from cachestoragelock import CacheStorageLock
from test_server import HTTPTestBase
#
# Module
#
class CacheShelveTest(HTTPTestBase):
def setUp(self):
HTTPTestBase.setUp(self)
handle, self.shelve_filename = tempfile.mkstemp('.shelve')
os.close(handle) # we just want the file name, so close the open handle
os.unlink(self.shelve_filename) # remove the empty file
return
def tearDown(self):
try:
os.unlink(self.shelve_filename)
except AttributeError:
pass
HTTPTestBase.tearDown(self)
return
def test(self):
storage = shelve.open(self.shelve_filename)
locking_storage = CacheStorageLock(storage)
try:
fc = Cache(locking_storage)
# First fetch the data through the cache
parsed_data = fc.fetch(self.TEST_URL)
self.failUnlessEqual(parsed_data.feed.title, 'CacheTest test data')
# Now retrieve the same data directly from the shelf
modified, shelved_data = storage[self.TEST_URL]
# The data should be the same
self.failUnlessEqual(parsed_data, shelved_data)
finally:
storage.close()
return
if __name__ == '__main__':
unittest.main()

View file

@ -1,241 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""Simple HTTP server for testing the feed cache.
"""
__module_id__ = "$Id$"
#
# Import system modules
#
import BaseHTTPServer
import logging
import md5
import threading
import time
import unittest
import urllib
#
# Import local modules
#
#
# Module
#
logger = logging.getLogger('feedcache.test_server')
def make_etag(data):
"""Given a string containing data to be returned to the client,
compute an ETag value for the data.
"""
_md5 = md5.new()
_md5.update(data)
return _md5.hexdigest()
class TestHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"HTTP request handler which serves the same feed data every time."
FEED_DATA = """<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">
<title>CacheTest test data</title>
<link href="http://localhost/feedcache/" rel="alternate"></link>
<link href="http://localhost/feedcache/atom/" rel="self"></link>
<id>http://localhost/feedcache/</id>
<updated>2006-10-14T11:00:36Z</updated>
<entry>
<title>single test entry</title>
<link href="http://www.example.com/" rel="alternate"></link>
<updated>2006-10-14T11:00:36Z</updated>
<author>
<name>author goes here</name>
<email>authoremail@example.com</email>
</author>
<id>http://www.example.com/</id>
<summary type="html">description goes here</summary>
<link length="100" href="http://www.example.com/enclosure" type="text/html" rel="enclosure">
</link>
</entry>
</feed>"""
# The data does not change, so save the ETag and modified times
# as class attributes.
ETAG = make_etag(FEED_DATA)
# Calculated using email.utils.formatdate(usegmt=True)
MODIFIED_TIME = 'Sun, 08 Apr 2012 20:16:48 GMT'
def do_GET(self):
"Handle GET requests."
logger.debug('GET %s', self.path)
if self.path == '/shutdown':
# Shortcut to handle stopping the server
logger.debug('Stopping server')
self.server.stop()
self.send_response(200)
else:
# Record the request for tests that count them
self.server.requests.append(self.path)
# Process the request
logger.debug('pre-defined response code: %d', self.server.response)
handler_method_name = 'do_GET_%d' % self.server.response
handler_method = getattr(self, handler_method_name)
handler_method()
return
def do_GET_3xx(self):
"Handle redirects"
if self.path.endswith('/redirected'):
logger.debug('already redirected')
# We have already redirected, so return the data.
return self.do_GET_200()
new_path = self.server.new_path
logger.debug('redirecting to %s', new_path)
self.send_response(self.server.response)
self.send_header('Location', new_path)
return
do_GET_301 = do_GET_3xx
do_GET_302 = do_GET_3xx
do_GET_303 = do_GET_3xx
do_GET_307 = do_GET_3xx
def do_GET_200(self):
logger.debug('Etag: %s' % self.ETAG)
logger.debug('Last-Modified: %s' % self.MODIFIED_TIME)
incoming_etag = self.headers.get('If-None-Match', None)
logger.debug('Incoming ETag: "%s"' % incoming_etag)
incoming_modified = self.headers.get('If-Modified-Since', None)
logger.debug('Incoming If-Modified-Since: %s' % incoming_modified)
send_data = True
# Does the client have the same version of the data we have?
if self.server.apply_modified_headers:
if incoming_etag == self.ETAG:
logger.debug('Response 304, etag')
self.send_response(304)
send_data = False
elif incoming_modified == self.MODIFIED_TIME:
logger.debug('Response 304, modified time')
self.send_response(304)
send_data = False
# Now optionally send the data, if the client needs it
if send_data:
logger.debug('Response 200')
self.send_response(200)
self.send_header('Content-Type', 'application/atom+xml')
logger.debug('Outgoing Etag: %s' % self.ETAG)
self.send_header('ETag', self.ETAG)
logger.debug('Outgoing modified time: %s' % self.MODIFIED_TIME)
self.send_header('Last-Modified', self.MODIFIED_TIME)
self.end_headers()
logger.debug('Sending data')
self.wfile.write(self.FEED_DATA)
return
class TestHTTPServer(BaseHTTPServer.HTTPServer):
"""HTTP Server which counts the number of requests made
and can stop based on client instructions.
"""
def __init__(self, applyModifiedHeaders=True, handler=TestHTTPHandler):
self.apply_modified_headers = applyModifiedHeaders
self.keep_serving = True
self.requests = []
self.setResponse(200)
BaseHTTPServer.HTTPServer.__init__(self, ('', 9999), handler)
return
def setResponse(self, newResponse, newPath=None):
"""Sets the response code to use for future requests, and a new
path to be used as a redirect target, if necessary.
"""
self.response = newResponse
self.new_path = newPath
return
def getNumRequests(self):
"Return the number of requests which have been made on the server."
return len(self.requests)
def stop(self):
"Stop serving requests, after the next request."
self.keep_serving = False
return
def serve_forever(self):
"Main loop for server"
while self.keep_serving:
self.handle_request()
logger.debug('exiting')
return
class HTTPTestBase(unittest.TestCase):
"Base class for tests that use a TestHTTPServer"
TEST_URL = 'http://localhost:9999/'
CACHE_TTL = 0
def setUp(self):
self.server = self.getServer()
self.server_thread = threading.Thread(target=self.server.serve_forever)
# set daemon flag so the tests don't hang if cleanup fails
self.server_thread.setDaemon(True)
self.server_thread.start()
return
def getServer(self):
"Return a web server for the test."
s = TestHTTPServer()
s.setResponse(200)
return s
def tearDown(self):
# Stop the server thread
urllib.urlretrieve(self.TEST_URL + 'shutdown')
time.sleep(1)
self.server.server_close()
self.server_thread.join()
return

View file

@ -1,89 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2007 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""Tests with shove filesystem storage.
"""
__module_id__ = "$Id$"
#
# Import system modules
#
import os
import shove
import tempfile
import threading
import unittest
#
# Import local modules
#
from cache import Cache
from test_server import HTTPTestBase
#
# Module
#
class CacheShoveTest(HTTPTestBase):
def setUp(self):
HTTPTestBase.setUp(self)
self.shove_dirname = tempfile.mkdtemp('shove')
return
def tearDown(self):
try:
os.system('rm -rf %s' % self.storage_dirname)
except AttributeError:
pass
HTTPTestBase.tearDown(self)
return
def test(self):
# First fetch the data through the cache
storage = shove.Shove('file://' + self.shove_dirname)
try:
fc = Cache(storage)
parsed_data = fc.fetch(self.TEST_URL)
self.failUnlessEqual(parsed_data.feed.title, 'CacheTest test data')
finally:
storage.close()
# Now retrieve the same data directly from the shelf
storage = shove.Shove('file://' + self.shove_dirname)
try:
modified, shelved_data = storage[self.TEST_URL]
finally:
storage.close()
# The data should be the same
self.failUnlessEqual(parsed_data, shelved_data)
return
if __name__ == '__main__':
unittest.main()

View file

@ -1,30 +0,0 @@
Metadata-Version: 1.1
Name: feedparser
Version: 5.1.3
Summary: Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds
Home-page: http://code.google.com/p/feedparser/
Author: Kurt McKee
Author-email: contactme@kurtmckee.org
License: UNKNOWN
Download-URL: http://code.google.com/p/feedparser/
Description: UNKNOWN
Keywords: atom,cdf,feed,parser,rdf,rss
Platform: POSIX
Platform: Windows
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.4
Classifier: Programming Language :: Python :: 2.5
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.0
Classifier: Programming Language :: Python :: 3.1
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Markup :: XML

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
feedparser

File diff suppressed because it is too large Load diff

View file

@ -1,859 +0,0 @@
#!/usr/bin/env python
__author__ = "Mark Pilgrim <http://diveintomark.org/>"
__license__ = """
Copyright (c) 2010-2012 Kurt McKee <contactme@kurtmckee.org>
Copyright (c) 2004-2008 Mark Pilgrim
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE."""
import codecs
import datetime
import glob
import operator
import os
import posixpath
import pprint
import re
import struct
import sys
import threading
import time
import unittest
import urllib
import warnings
import zlib
import BaseHTTPServer
import SimpleHTTPServer
import feedparser
if not feedparser._XML_AVAILABLE:
sys.stderr.write('No XML parsers available, unit testing can not proceed\n')
sys.exit(1)
try:
# the utf_32 codec was introduced in Python 2.6; it's necessary to
# check this as long as feedparser supports Python 2.4 and 2.5
codecs.lookup('utf_32')
except LookupError:
_UTF32_AVAILABLE = False
else:
_UTF32_AVAILABLE = True
_s2bytes = feedparser._s2bytes
_l2bytes = feedparser._l2bytes
#---------- custom HTTP server (used to serve test feeds) ----------
_PORT = 8097 # not really configurable, must match hardcoded port in tests
_HOST = '127.0.0.1' # also not really configurable
class FeedParserTestRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
headers_re = re.compile(_s2bytes(r"^Header:\s+([^:]+):(.+)$"), re.MULTILINE)
def send_head(self):
"""Send custom headers defined in test case
Example:
<!--
Header: Content-type: application/atom+xml
Header: X-Foo: bar
-->
"""
# Short-circuit the HTTP status test `test_redirect_to_304()`
if self.path == '/-/return-304.xml':
self.send_response(304)
self.send_header('Content-type', 'text/xml')
self.end_headers()
return feedparser._StringIO(u''.encode('utf-8'))
path = self.translate_path(self.path)
# the compression tests' filenames determine the header sent
if self.path.startswith('/tests/compression'):
if self.path.endswith('gz'):
headers = {'Content-Encoding': 'gzip'}
else:
headers = {'Content-Encoding': 'deflate'}
headers['Content-type'] = 'application/xml'
else:
headers = dict([(k.decode('utf-8'), v.decode('utf-8').strip()) for k, v in self.headers_re.findall(open(path, 'rb').read())])
f = open(path, 'rb')
if (self.headers.get('if-modified-since') == headers.get('Last-Modified', 'nom')) \
or (self.headers.get('if-none-match') == headers.get('ETag', 'nomatch')):
status = 304
else:
status = 200
headers.setdefault('Status', status)
self.send_response(int(headers['Status']))
headers.setdefault('Content-type', self.guess_type(path))
self.send_header("Content-type", headers['Content-type'])
self.send_header("Content-Length", str(os.stat(f.name)[6]))
for k, v in headers.items():
if k not in ('Status', 'Content-type'):
self.send_header(k, v)
self.end_headers()
return f
def log_request(self, *args):
pass
class FeedParserTestServer(threading.Thread):
"""HTTP Server that runs in a thread and handles a predetermined number of requests"""
def __init__(self, requests):
threading.Thread.__init__(self)
self.requests = requests
self.ready = threading.Event()
def run(self):
self.httpd = BaseHTTPServer.HTTPServer((_HOST, _PORT), FeedParserTestRequestHandler)
self.ready.set()
while self.requests:
self.httpd.handle_request()
self.requests -= 1
self.ready.clear()
#---------- dummy test case class (test methods are added dynamically) ----------
unicode1_re = re.compile(_s2bytes(" u'"))
unicode2_re = re.compile(_s2bytes(' u"'))
# _bytes is only used in everythingIsUnicode().
# In Python 2 it's str, and in Python 3 it's bytes.
_bytes = type(_s2bytes(''))
def everythingIsUnicode(d):
"""Takes a dictionary, recursively verifies that every value is unicode"""
for k, v in d.iteritems():
if isinstance(v, dict) and k != 'headers':
if not everythingIsUnicode(v):
return False
elif isinstance(v, list):
for i in v:
if isinstance(i, dict) and not everythingIsUnicode(i):
return False
elif isinstance(i, _bytes):
return False
elif isinstance(v, _bytes):
return False
return True
def failUnlessEval(self, xmlfile, evalString, msg=None):
"""Fail unless eval(evalString, env)"""
env = feedparser.parse(xmlfile)
try:
if not eval(evalString, globals(), env):
failure=(msg or 'not eval(%s) \nWITH env(%s)' % (evalString, pprint.pformat(env)))
raise self.failureException, failure
if not everythingIsUnicode(env):
raise self.failureException, "not everything is unicode \nWITH env(%s)" % (pprint.pformat(env), )
except SyntaxError:
# Python 3 doesn't have the `u""` syntax, so evalString needs to be modified,
# which will require the failure message to be updated
evalString = re.sub(unicode1_re, _s2bytes(" '"), evalString)
evalString = re.sub(unicode2_re, _s2bytes(' "'), evalString)
if not eval(evalString, globals(), env):
failure=(msg or 'not eval(%s) \nWITH env(%s)' % (evalString, pprint.pformat(env)))
raise self.failureException, failure
class BaseTestCase(unittest.TestCase):
failUnlessEval = failUnlessEval
class TestCase(BaseTestCase):
pass
class TestTemporaryFallbackBehavior(unittest.TestCase):
"These tests are temporarily here because of issues 310 and 328"
def test_issue_328_fallback_behavior(self):
warnings.filterwarnings('error')
d = feedparser.FeedParserDict()
d['published'] = u'pub string'
d['published_parsed'] = u'pub tuple'
d['updated'] = u'upd string'
d['updated_parsed'] = u'upd tuple'
# Ensure that `updated` doesn't map to `published` when it exists
self.assertTrue('published' in d)
self.assertTrue('published_parsed' in d)
self.assertTrue('updated' in d)
self.assertTrue('updated_parsed' in d)
self.assertEqual(d['published'], 'pub string')
self.assertEqual(d['published_parsed'], 'pub tuple')
self.assertEqual(d['updated'], 'upd string')
self.assertEqual(d['updated_parsed'], 'upd tuple')
d = feedparser.FeedParserDict()
d['published'] = u'pub string'
d['published_parsed'] = u'pub tuple'
# Ensure that `updated` doesn't actually exist
self.assertTrue('updated' not in d)
self.assertTrue('updated_parsed' not in d)
# Ensure that accessing `updated` throws a DeprecationWarning
try:
d['updated']
except DeprecationWarning:
# Expected behavior
pass
else:
# Wrong behavior
self.assertEqual(True, False)
try:
d['updated_parsed']
except DeprecationWarning:
# Expected behavior
pass
else:
# Wrong behavior
self.assertEqual(True, False)
# Ensure that `updated` maps to `published`
warnings.filterwarnings('ignore')
self.assertEqual(d['updated'], u'pub string')
self.assertEqual(d['updated_parsed'], u'pub tuple')
warnings.resetwarnings()
class TestEverythingIsUnicode(unittest.TestCase):
"Ensure that `everythingIsUnicode()` is working appropriately"
def test_everything_is_unicode(self):
self.assertTrue(everythingIsUnicode(
{'a': u'a', 'b': [u'b', {'c': u'c'}], 'd': {'e': u'e'}}
))
def test_not_everything_is_unicode(self):
self.assertFalse(everythingIsUnicode({'a': _s2bytes('a')}))
self.assertFalse(everythingIsUnicode({'a': [_s2bytes('a')]}))
self.assertFalse(everythingIsUnicode({'a': {'b': _s2bytes('b')}}))
self.assertFalse(everythingIsUnicode({'a': [{'b': _s2bytes('b')}]}))
class TestLooseParser(BaseTestCase):
"Test the sgmllib-based parser by manipulating feedparser " \
"into believing no XML parsers are installed"
def __init__(self, arg):
unittest.TestCase.__init__(self, arg)
self._xml_available = feedparser._XML_AVAILABLE
def setUp(self):
feedparser._XML_AVAILABLE = 0
def tearDown(self):
feedparser._XML_AVAILABLE = self._xml_available
class TestStrictParser(BaseTestCase):
pass
class TestMicroformats(BaseTestCase):
pass
class TestEncodings(BaseTestCase):
def test_doctype_replacement(self):
"Ensure that non-ASCII-compatible encodings don't hide " \
"disallowed ENTITY declarations"
doc = """<?xml version="1.0" encoding="utf-16be"?>
<!DOCTYPE feed [
<!ENTITY exponential1 "bogus ">
<!ENTITY exponential2 "&exponential1;&exponential1;">
<!ENTITY exponential3 "&exponential2;&exponential2;">
]>
<feed><title type="html">&exponential3;</title></feed>"""
doc = codecs.BOM_UTF16_BE + doc.encode('utf-16be')
result = feedparser.parse(doc)
self.assertEqual(result['feed']['title'], u'&amp;exponential3')
def test_gb2312_converted_to_gb18030_in_xml_encoding(self):
# \u55de was chosen because it exists in gb18030 but not gb2312
feed = u'''<?xml version="1.0" encoding="gb2312"?>
<feed><title>\u55de</title></feed>'''
result = feedparser.parse(feed.encode('gb18030'), response_headers={
'Content-Type': 'text/xml'
})
self.assertEqual(result.encoding, 'gb18030')
class TestFeedParserDict(unittest.TestCase):
"Ensure that FeedParserDict returns values as expected and won't crash"
def setUp(self):
self.d = feedparser.FeedParserDict()
def _check_key(self, k):
self.assertTrue(k in self.d)
self.assertTrue(hasattr(self.d, k))
self.assertEqual(self.d[k], 1)
self.assertEqual(getattr(self.d, k), 1)
def _check_no_key(self, k):
self.assertTrue(k not in self.d)
self.assertTrue(not hasattr(self.d, k))
def test_empty(self):
keys = (
'a','entries', 'id', 'guid', 'summary', 'subtitle', 'description',
'category', 'enclosures', 'license', 'categories',
)
for k in keys:
self._check_no_key(k)
self.assertTrue('items' not in self.d)
self.assertTrue(hasattr(self.d, 'items')) # dict.items() exists
def test_neutral(self):
self.d['a'] = 1
self._check_key('a')
def test_single_mapping_target_1(self):
self.d['id'] = 1
self._check_key('id')
self._check_key('guid')
def test_single_mapping_target_2(self):
self.d['guid'] = 1
self._check_key('id')
self._check_key('guid')
def test_multiple_mapping_target_1(self):
self.d['summary'] = 1
self._check_key('summary')
self._check_key('description')
def test_multiple_mapping_target_2(self):
self.d['subtitle'] = 1
self._check_key('subtitle')
self._check_key('description')
def test_multiple_mapping_mapped_key(self):
self.d['description'] = 1
self._check_key('summary')
self._check_key('description')
def test_license(self):
self.d['links'] = []
try:
self.d['license']
self.assertTrue(False)
except KeyError:
pass
self.d['links'].append({'rel': 'license'})
try:
self.d['license']
self.assertTrue(False)
except KeyError:
pass
self.d['links'].append({'rel': 'license', 'href': 'http://dom.test/'})
self.assertEqual(self.d['license'], 'http://dom.test/')
def test_category(self):
self.d['tags'] = []
try:
self.d['category']
self.assertTrue(False)
except KeyError:
pass
self.d['tags'] = [{}]
try:
self.d['category']
self.assertTrue(False)
except KeyError:
pass
self.d['tags'] = [{'term': 'cat'}]
self.assertEqual(self.d['category'], 'cat')
self.d['tags'].append({'term': 'dog'})
self.assertEqual(self.d['category'], 'cat')
class TestOpenResource(unittest.TestCase):
"Ensure that `_open_resource()` interprets its arguments as URIs, " \
"file-like objects, or in-memory feeds as expected"
def test_fileobj(self):
r = feedparser._open_resource(sys.stdin, '', '', '', '', [], {})
self.assertTrue(r is sys.stdin)
def test_feed(self):
f = feedparser.parse(u'feed://localhost:8097/tests/http/target.xml')
self.assertEqual(f.href, u'http://localhost:8097/tests/http/target.xml')
def test_feed_http(self):
f = feedparser.parse(u'feed:http://localhost:8097/tests/http/target.xml')
self.assertEqual(f.href, u'http://localhost:8097/tests/http/target.xml')
def test_bytes(self):
s = '<feed><item><title>text</title></item></feed>'.encode('utf-8')
r = feedparser._open_resource(s, '', '', '', '', [], {})
self.assertEqual(s, r.read())
def test_string(self):
s = '<feed><item><title>text</title></item></feed>'
r = feedparser._open_resource(s, '', '', '', '', [], {})
self.assertEqual(s.encode('utf-8'), r.read())
def test_unicode_1(self):
s = u'<feed><item><title>text</title></item></feed>'
r = feedparser._open_resource(s, '', '', '', '', [], {})
self.assertEqual(s.encode('utf-8'), r.read())
def test_unicode_2(self):
s = u'<feed><item><title>t\u00e9xt</title></item></feed>'
r = feedparser._open_resource(s, '', '', '', '', [], {})
self.assertEqual(s.encode('utf-8'), r.read())
class TestMakeSafeAbsoluteURI(unittest.TestCase):
"Exercise the URI joining and sanitization code"
base = u'http://d.test/d/f.ext'
def _mktest(rel, expect, doc):
def fn(self):
value = feedparser._makeSafeAbsoluteURI(self.base, rel)
self.assertEqual(value, expect)
fn.__doc__ = doc
return fn
# make the test cases; the call signature is:
# (relative_url, expected_return_value, test_doc_string)
test_abs = _mktest(u'https://s.test/', u'https://s.test/', 'absolute uri')
test_rel = _mktest(u'/new', u'http://d.test/new', 'relative uri')
test_bad = _mktest(u'x://bad.test/', u'', 'unacceptable uri protocol')
test_mag = _mktest(u'magnet:?xt=a', u'magnet:?xt=a', 'magnet uri')
def test_catch_ValueError(self):
'catch ValueError in Python 2.7 and up'
uri = u'http://bad]test/'
value1 = feedparser._makeSafeAbsoluteURI(uri)
value2 = feedparser._makeSafeAbsoluteURI(self.base, uri)
swap = feedparser.ACCEPTABLE_URI_SCHEMES
feedparser.ACCEPTABLE_URI_SCHEMES = ()
value3 = feedparser._makeSafeAbsoluteURI(self.base, uri)
feedparser.ACCEPTABLE_URI_SCHEMES = swap
# Only Python 2.7 and up throw a ValueError, otherwise uri is returned
self.assertTrue(value1 in (uri, u''))
self.assertTrue(value2 in (uri, u''))
self.assertTrue(value3 in (uri, u''))
class TestConvertToIdn(unittest.TestCase):
"Test IDN support (unavailable in Jython as of Jython 2.5.2)"
# this is the greek test domain
hostname = u'\u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1'
hostname += u'.\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae'
def test_control(self):
r = feedparser._convert_to_idn(u'http://example.test/')
self.assertEqual(r, u'http://example.test/')
def test_idn(self):
r = feedparser._convert_to_idn(u'http://%s/' % (self.hostname,))
self.assertEqual(r, u'http://xn--hxajbheg2az3al.xn--jxalpdlp/')
def test_port(self):
r = feedparser._convert_to_idn(u'http://%s:8080/' % (self.hostname,))
self.assertEqual(r, u'http://xn--hxajbheg2az3al.xn--jxalpdlp:8080/')
class TestCompression(unittest.TestCase):
"Test the gzip and deflate support in the HTTP code"
def test_gzip_good(self):
f = feedparser.parse('http://localhost:8097/tests/compression/gzip.gz')
self.assertEqual(f.version, 'atom10')
def test_gzip_not_compressed(self):
f = feedparser.parse('http://localhost:8097/tests/compression/gzip-not-compressed.gz')
self.assertEqual(f.bozo, 1)
self.assertTrue(isinstance(f.bozo_exception, IOError))
self.assertEqual(f['feed']['title'], 'gzip')
def test_gzip_struct_error(self):
f = feedparser.parse('http://localhost:8097/tests/compression/gzip-struct-error.gz')
self.assertEqual(f.bozo, 1)
self.assertTrue(isinstance(f.bozo_exception, struct.error))
def test_zlib_good(self):
f = feedparser.parse('http://localhost:8097/tests/compression/deflate.z')
self.assertEqual(f.version, 'atom10')
def test_zlib_no_headers(self):
f = feedparser.parse('http://localhost:8097/tests/compression/deflate-no-headers.z')
self.assertEqual(f.version, 'atom10')
def test_zlib_not_compressed(self):
f = feedparser.parse('http://localhost:8097/tests/compression/deflate-not-compressed.z')
self.assertEqual(f.bozo, 1)
self.assertTrue(isinstance(f.bozo_exception, zlib.error))
self.assertEqual(f['feed']['title'], 'deflate')
class TestHTTPStatus(unittest.TestCase):
"Test HTTP redirection and other status codes"
def test_301(self):
f = feedparser.parse('http://localhost:8097/tests/http/http_status_301.xml')
self.assertEqual(f.status, 301)
self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')
self.assertEqual(f.entries[0].title, 'target')
def test_302(self):
f = feedparser.parse('http://localhost:8097/tests/http/http_status_302.xml')
self.assertEqual(f.status, 302)
self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')
self.assertEqual(f.entries[0].title, 'target')
def test_303(self):
f = feedparser.parse('http://localhost:8097/tests/http/http_status_303.xml')
self.assertEqual(f.status, 303)
self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')
self.assertEqual(f.entries[0].title, 'target')
def test_307(self):
f = feedparser.parse('http://localhost:8097/tests/http/http_status_307.xml')
self.assertEqual(f.status, 307)
self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')
self.assertEqual(f.entries[0].title, 'target')
def test_304(self):
# first retrieve the url
u = 'http://localhost:8097/tests/http/http_status_304.xml'
f = feedparser.parse(u)
self.assertEqual(f.status, 200)
self.assertEqual(f.entries[0].title, 'title 304')
# extract the etag and last-modified headers
e = [v for k, v in f.headers.items() if k.lower() == 'etag'][0]
mh = [v for k, v in f.headers.items() if k.lower() == 'last-modified'][0]
ms = f.updated
mt = f.updated_parsed
md = datetime.datetime(*mt[0:7])
self.assertTrue(isinstance(mh, basestring))
self.assertTrue(isinstance(ms, basestring))
self.assertTrue(isinstance(mt, time.struct_time))
self.assertTrue(isinstance(md, datetime.datetime))
# test that sending back the etag results in a 304
f = feedparser.parse(u, etag=e)
self.assertEqual(f.status, 304)
# test that sending back last-modified (string) results in a 304
f = feedparser.parse(u, modified=ms)
self.assertEqual(f.status, 304)
# test that sending back last-modified (9-tuple) results in a 304
f = feedparser.parse(u, modified=mt)
self.assertEqual(f.status, 304)
# test that sending back last-modified (datetime) results in a 304
f = feedparser.parse(u, modified=md)
self.assertEqual(f.status, 304)
def test_404(self):
f = feedparser.parse('http://localhost:8097/tests/http/http_status_404.xml')
self.assertEqual(f.status, 404)
def test_9001(self):
f = feedparser.parse('http://localhost:8097/tests/http/http_status_9001.xml')
self.assertEqual(f.bozo, 1)
def test_redirect_to_304(self):
# ensure that an http redirect to an http 304 doesn't
# trigger a bozo_exception
u = 'http://localhost:8097/tests/http/http_redirect_to_304.xml'
f = feedparser.parse(u)
self.assertTrue(f.bozo == 0)
self.assertTrue(f.status == 302)
class TestDateParsers(unittest.TestCase):
"Test the various date parsers; most of the test cases are constructed " \
"dynamically based on the contents of the `date_tests` dict, below"
def test_None(self):
self.assertTrue(feedparser._parse_date(None) is None)
def _check_date(self, func, dtstring, dttuple):
try:
tup = func(dtstring)
except (OverflowError, ValueError):
tup = None
self.assertEqual(tup, dttuple)
self.assertEqual(tup, feedparser._parse_date(dtstring))
def test_year_10000_date(self):
# On some systems this date string will trigger an OverflowError.
# On Jython and x64 systems, however, it's interpreted just fine.
try:
date = feedparser._parse_date_rfc822(u'Sun, 31 Dec 9999 23:59:59 -9999')
except OverflowError:
date = None
self.assertTrue(date in (None, (10000, 1, 5, 4, 38, 59, 2, 5, 0)))
date_tests = {
feedparser._parse_date_greek: (
(u'', None), # empty string
(u'\u039a\u03c5\u03c1, 11 \u0399\u03bf\u03cd\u03bb 2004 12:00:00 EST', (2004, 7, 11, 17, 0, 0, 6, 193, 0)),
),
feedparser._parse_date_hungarian: (
(u'', None), # empty string
(u'2004-j\u00falius-13T9:15-05:00', (2004, 7, 13, 14, 15, 0, 1, 195, 0)),
),
feedparser._parse_date_iso8601: (
(u'', None), # empty string
(u'-0312', (2003, 12, 1, 0, 0, 0, 0, 335, 0)), # 2-digit year/month only variant
(u'031231', (2003, 12, 31, 0, 0, 0, 2, 365, 0)), # 2-digit year/month/day only, no hyphens
(u'03-12-31', (2003, 12, 31, 0, 0, 0, 2, 365, 0)), # 2-digit year/month/day only
(u'-03-12', (2003, 12, 1, 0, 0, 0, 0, 335, 0)), # 2-digit year/month only
(u'03335', (2003, 12, 1, 0, 0, 0, 0, 335, 0)), # 2-digit year/ordinal, no hyphens
(u'2003-12-31T10:14:55.1234Z', (2003, 12, 31, 10, 14, 55, 2, 365, 0)), # fractional seconds
# Special case for Google's extra zero in the month
(u'2003-012-31T10:14:55+00:00', (2003, 12, 31, 10, 14, 55, 2, 365, 0)),
),
feedparser._parse_date_nate: (
(u'', None), # empty string
(u'2004-05-25 \uc624\ud6c4 11:23:17', (2004, 5, 25, 14, 23, 17, 1, 146, 0)),
),
feedparser._parse_date_onblog: (
(u'', None), # empty string
(u'2004\ub144 05\uc6d4 28\uc77c 01:31:15', (2004, 5, 27, 16, 31, 15, 3, 148, 0)),
),
feedparser._parse_date_perforce: (
(u'', None), # empty string
(u'Fri, 2006/09/15 08:19:53 EDT', (2006, 9, 15, 12, 19, 53, 4, 258, 0)),
),
feedparser._parse_date_rfc822: (
(u'', None), # empty string
(u'Thu, 01 Jan 0100 00:00:01 +0100', (99, 12, 31, 23, 0, 1, 3, 365, 0)), # ancient date
(u'Thu, 01 Jan 04 19:48:21 GMT', (2004, 1, 1, 19, 48, 21, 3, 1, 0)), # 2-digit year
(u'Thu, 01 Jan 2004 19:48:21 GMT', (2004, 1, 1, 19, 48, 21, 3, 1, 0)), # 4-digit year
(u'Thu, 5 Apr 2012 10:00:00 GMT', (2012, 4, 5, 10, 0, 0, 3, 96, 0)), # 1-digit day
(u'Wed, 19 Aug 2009 18:28:00 Etc/GMT', (2009, 8, 19, 18, 28, 0, 2, 231, 0)), # etc/gmt timezone
(u'Wed, 19 Feb 2012 22:40:00 GMT-01:01', (2012, 2, 19, 23, 41, 0, 6, 50, 0)), # gmt+hh:mm timezone
(u'Mon, 13 Feb, 2012 06:28:00 UTC', (2012, 2, 13, 6, 28, 0, 0, 44, 0)), # extraneous comma
(u'Thu, 01 Jan 2004 00:00 GMT', (2004, 1, 1, 0, 0, 0, 3, 1, 0)), # no seconds
(u'Thu, 01 Jan 2004', (2004, 1, 1, 0, 0, 0, 3, 1, 0)), # no time
# Additional tests to handle Disney's long month names and invalid timezones
(u'Mon, 26 January 2004 16:31:00 AT', (2004, 1, 26, 20, 31, 0, 0, 26, 0)),
(u'Mon, 26 January 2004 16:31:00 ET', (2004, 1, 26, 21, 31, 0, 0, 26, 0)),
(u'Mon, 26 January 2004 16:31:00 CT', (2004, 1, 26, 22, 31, 0, 0, 26, 0)),
(u'Mon, 26 January 2004 16:31:00 MT', (2004, 1, 26, 23, 31, 0, 0, 26, 0)),
(u'Mon, 26 January 2004 16:31:00 PT', (2004, 1, 27, 0, 31, 0, 1, 27, 0)),
),
feedparser._parse_date_rfc822_grubby: (
(u'Thu Aug 30 2012 17:26:16 +0200', (2012, 8, 30, 15, 26, 16, 3, 243, 0)),
),
feedparser._parse_date_asctime: (
(u'Sun Jan 4 16:29:06 2004', (2004, 1, 4, 16, 29, 6, 6, 4, 0)),
),
feedparser._parse_date_w3dtf: (
(u'', None), # empty string
(u'2003-12-31T10:14:55Z', (2003, 12, 31, 10, 14, 55, 2, 365, 0)), # UTC
(u'2003-12-31T10:14:55-08:00', (2003, 12, 31, 18, 14, 55, 2, 365, 0)), # San Francisco timezone
(u'2003-12-31T18:14:55+08:00', (2003, 12, 31, 10, 14, 55, 2, 365, 0)), # Tokyo timezone
(u'2007-04-23T23:25:47.538+10:00', (2007, 4, 23, 13, 25, 47, 0, 113, 0)), # fractional seconds
(u'2003-12-31', (2003, 12, 31, 0, 0, 0, 2, 365, 0)), # year/month/day only
(u'20031231', (2003, 12, 31, 0, 0, 0, 2, 365, 0)), # year/month/day only, no hyphens
(u'2003-12', (2003, 12, 1, 0, 0, 0, 0, 335, 0)), # year/month only
(u'2003', (2003, 1, 1, 0, 0, 0, 2, 1, 0)), # year only
# MSSQL-style dates
(u'2004-07-08 23:56:58 -00:20', (2004, 7, 9, 0, 16, 58, 4, 191, 0)), # with timezone
(u'2004-07-08 23:56:58', (2004, 7, 8, 23, 56, 58, 3, 190, 0)), # without timezone
(u'2004-07-08 23:56:58.0', (2004, 7, 8, 23, 56, 58, 3, 190, 0)), # with fractional second
# Special cases for out-of-range times
(u'2003-12-31T25:14:55Z', (2004, 1, 1, 1, 14, 55, 3, 1, 0)), # invalid (25 hours)
(u'2003-12-31T10:61:55Z', (2003, 12, 31, 11, 1, 55, 2, 365, 0)), # invalid (61 minutes)
(u'2003-12-31T10:14:61Z', (2003, 12, 31, 10, 15, 1, 2, 365, 0)), # invalid (61 seconds)
# Special cases for rollovers in leap years
(u'2004-02-28T18:14:55-08:00', (2004, 2, 29, 2, 14, 55, 6, 60, 0)), # feb 28 in leap year
(u'2003-02-28T18:14:55-08:00', (2003, 3, 1, 2, 14, 55, 5, 60, 0)), # feb 28 in non-leap year
(u'2000-02-28T18:14:55-08:00', (2000, 2, 29, 2, 14, 55, 1, 60, 0)), # feb 28 in leap year on century divisible by 400
)
}
def make_date_test(f, s, t):
return lambda self: self._check_date(f, s, t)
for func, items in date_tests.iteritems():
for i, (dtstring, dttuple) in enumerate(items):
uniqfunc = make_date_test(func, dtstring, dttuple)
setattr(TestDateParsers, 'test_%s_%02i' % (func.__name__, i), uniqfunc)
class TestHTMLGuessing(unittest.TestCase):
"Exercise the HTML sniffing code"
def _mktest(text, expect, doc):
def fn(self):
value = bool(feedparser._FeedParserMixin.lookslikehtml(text))
self.assertEqual(value, expect)
fn.__doc__ = doc
return fn
test_text_1 = _mktest(u'plain text', False, u'plain text')
test_text_2 = _mktest(u'2 < 3', False, u'plain text with angle bracket')
test_html_1 = _mktest(u'<a href="">a</a>', True, u'anchor tag')
test_html_2 = _mktest(u'<i>i</i>', True, u'italics tag')
test_html_3 = _mktest(u'<b>b</b>', True, u'bold tag')
test_html_4 = _mktest(u'<code>', False, u'allowed tag, no end tag')
test_html_5 = _mktest(u'<rss> .. </rss>', False, u'disallowed tag')
test_entity_1 = _mktest(u'AT&T', False, u'corporation name')
test_entity_2 = _mktest(u'&copy;', True, u'named entity reference')
test_entity_3 = _mktest(u'&#169;', True, u'numeric entity reference')
test_entity_4 = _mktest(u'&#xA9;', True, u'hex numeric entity reference')
#---------- additional api unit tests, not backed by files
class TestBuildRequest(unittest.TestCase):
"Test that HTTP request objects are created as expected"
def test_extra_headers(self):
"""You can pass in extra headers and they go into the request object."""
request = feedparser._build_urllib2_request(
'http://example.com/feed',
'agent-name',
None, None, None, None,
{'Cache-Control': 'max-age=0'})
# nb, urllib2 folds the case of the headers
self.assertEqual(
request.get_header('Cache-control'), 'max-age=0')
class TestLxmlBug(unittest.TestCase):
def test_lxml_etree_bug(self):
try:
import lxml.etree
except ImportError:
pass
else:
doc = u"<feed>&illformed_charref</feed>".encode('utf8')
# Importing lxml.etree currently causes libxml2 to
# throw SAXException instead of SAXParseException.
feedparser.parse(feedparser._StringIO(doc))
self.assertTrue(True)
#---------- parse test files and create test methods ----------
def convert_to_utf8(data):
"Identify data's encoding using its byte order mark" \
"and convert it to its utf-8 equivalent"
if data[:4] == _l2bytes([0x4c, 0x6f, 0xa7, 0x94]):
return data.decode('cp037').encode('utf-8')
elif data[:4] == _l2bytes([0x00, 0x00, 0xfe, 0xff]):
if not _UTF32_AVAILABLE:
return None
return data.decode('utf-32be').encode('utf-8')
elif data[:4] == _l2bytes([0xff, 0xfe, 0x00, 0x00]):
if not _UTF32_AVAILABLE:
return None
return data.decode('utf-32le').encode('utf-8')
elif data[:4] == _l2bytes([0x00, 0x00, 0x00, 0x3c]):
if not _UTF32_AVAILABLE:
return None
return data.decode('utf-32be').encode('utf-8')
elif data[:4] == _l2bytes([0x3c, 0x00, 0x00, 0x00]):
if not _UTF32_AVAILABLE:
return None
return data.decode('utf-32le').encode('utf-8')
elif data[:4] == _l2bytes([0x00, 0x3c, 0x00, 0x3f]):
return data.decode('utf-16be').encode('utf-8')
elif data[:4] == _l2bytes([0x3c, 0x00, 0x3f, 0x00]):
return data.decode('utf-16le').encode('utf-8')
elif (data[:2] == _l2bytes([0xfe, 0xff])) and (data[2:4] != _l2bytes([0x00, 0x00])):
return data[2:].decode('utf-16be').encode('utf-8')
elif (data[:2] == _l2bytes([0xff, 0xfe])) and (data[2:4] != _l2bytes([0x00, 0x00])):
return data[2:].decode('utf-16le').encode('utf-8')
elif data[:3] == _l2bytes([0xef, 0xbb, 0xbf]):
return data[3:]
# no byte order mark was found
return data
skip_re = re.compile(_s2bytes("SkipUnless:\s*(.*?)\n"))
desc_re = re.compile(_s2bytes("Description:\s*(.*?)\s*Expect:\s*(.*)\s*-->"))
def getDescription(xmlfile, data):
"""Extract test data
Each test case is an XML file which contains not only a test feed
but also the description of the test and the condition that we
would expect the parser to create when it parses the feed. Example:
<!--
Description: feed title
Expect: feed['title'] == u'Example feed'
-->
"""
skip_results = skip_re.search(data)
if skip_results:
skipUnless = skip_results.group(1).strip()
else:
skipUnless = '1'
search_results = desc_re.search(data)
if not search_results:
raise RuntimeError, "can't parse %s" % xmlfile
description, evalString = map(lambda s: s.strip(), list(search_results.groups()))
description = xmlfile + ": " + unicode(description, 'utf8')
return description, evalString, skipUnless
def buildTestCase(xmlfile, description, evalString):
func = lambda self, xmlfile=xmlfile, evalString=evalString: \
self.failUnlessEval(xmlfile, evalString)
func.__doc__ = description
return func
def runtests():
"Read the files in the tests/ directory, dynamically add tests to the " \
"TestCases above, spawn the HTTP server, and run the test suite"
if sys.argv[1:]:
allfiles = filter(lambda s: s.endswith('.xml'), reduce(operator.add, map(glob.glob, sys.argv[1:]), []))
sys.argv = [sys.argv[0]] #+ sys.argv[2:]
else:
allfiles = glob.glob(os.path.join('.', 'tests', '**', '**', '*.xml'))
wellformedfiles = glob.glob(os.path.join('.', 'tests', 'wellformed', '**', '*.xml'))
illformedfiles = glob.glob(os.path.join('.', 'tests', 'illformed', '*.xml'))
encodingfiles = glob.glob(os.path.join('.', 'tests', 'encoding', '*.xml'))
entitiesfiles = glob.glob(os.path.join('.', 'tests', 'entities', '*.xml'))
microformatfiles = glob.glob(os.path.join('.', 'tests', 'microformats', '**', '*.xml'))
httpd = None
# there are several compression test cases that must be accounted for
# as well as a number of http status tests that redirect to a target
# and a few `_open_resource`-related tests
httpcount = 6 + 17 + 2
httpcount += len([f for f in allfiles if 'http' in f])
httpcount += len([f for f in wellformedfiles if 'http' in f])
httpcount += len([f for f in illformedfiles if 'http' in f])
httpcount += len([f for f in encodingfiles if 'http' in f])
try:
for c, xmlfile in enumerate(allfiles + encodingfiles + illformedfiles + entitiesfiles):
addTo = TestCase
if xmlfile in encodingfiles:
addTo = TestEncodings
elif xmlfile in entitiesfiles:
addTo = (TestStrictParser, TestLooseParser)
elif xmlfile in microformatfiles:
addTo = TestMicroformats
elif xmlfile in wellformedfiles:
addTo = (TestStrictParser, TestLooseParser)
data = open(xmlfile, 'rb').read()
if 'encoding' in xmlfile:
data = convert_to_utf8(data)
if data is None:
# convert_to_utf8 found a byte order mark for utf_32
# but it's not supported in this installation of Python
if 'http' in xmlfile:
httpcount -= 1 + (xmlfile in wellformedfiles)
continue
description, evalString, skipUnless = getDescription(xmlfile, data)
testName = 'test_%06d' % c
ishttp = 'http' in xmlfile
try:
if not eval(skipUnless): raise NotImplementedError
except (ImportError, LookupError, NotImplementedError, AttributeError):
if ishttp:
httpcount -= 1 + (xmlfile in wellformedfiles)
continue
if ishttp:
xmlfile = 'http://%s:%s/%s' % (_HOST, _PORT, posixpath.normpath(xmlfile.replace('\\', '/')))
testFunc = buildTestCase(xmlfile, description, evalString)
if isinstance(addTo, tuple):
setattr(addTo[0], testName, testFunc)
setattr(addTo[1], testName, testFunc)
else:
setattr(addTo, testName, testFunc)
if feedparser.TIDY_MARKUP and feedparser._mxtidy:
sys.stderr.write('\nWarning: feedparser.TIDY_MARKUP invalidates tests, turning it off temporarily\n\n')
feedparser.TIDY_MARKUP = 0
if httpcount:
httpd = FeedParserTestServer(httpcount)
httpd.daemon = True
httpd.start()
httpd.ready.wait()
testsuite = unittest.TestSuite()
testloader = unittest.TestLoader()
testsuite.addTest(testloader.loadTestsFromTestCase(TestCase))
testsuite.addTest(testloader.loadTestsFromTestCase(TestStrictParser))
testsuite.addTest(testloader.loadTestsFromTestCase(TestLooseParser))
testsuite.addTest(testloader.loadTestsFromTestCase(TestEncodings))
testsuite.addTest(testloader.loadTestsFromTestCase(TestDateParsers))
testsuite.addTest(testloader.loadTestsFromTestCase(TestHTMLGuessing))
testsuite.addTest(testloader.loadTestsFromTestCase(TestHTTPStatus))
testsuite.addTest(testloader.loadTestsFromTestCase(TestCompression))
testsuite.addTest(testloader.loadTestsFromTestCase(TestConvertToIdn))
testsuite.addTest(testloader.loadTestsFromTestCase(TestMicroformats))
testsuite.addTest(testloader.loadTestsFromTestCase(TestOpenResource))
testsuite.addTest(testloader.loadTestsFromTestCase(TestFeedParserDict))
testsuite.addTest(testloader.loadTestsFromTestCase(TestMakeSafeAbsoluteURI))
testsuite.addTest(testloader.loadTestsFromTestCase(TestEverythingIsUnicode))
testsuite.addTest(testloader.loadTestsFromTestCase(TestTemporaryFallbackBehavior))
testsuite.addTest(testloader.loadTestsFromTestCase(TestLxmlBug))
testresults = unittest.TextTestRunner(verbosity=1).run(testsuite)
# Return 0 if successful, 1 if there was a failure
sys.exit(not testresults.wasSuccessful())
finally:
if httpd:
if httpd.requests:
# Should never get here unless something went horribly wrong, like the
# user hitting Ctrl-C. Tell our HTTP server that it's done, then do
# one more request to flush it. This rarely works; the combination of
# threading, self-terminating HTTP servers, and unittest is really
# quite flaky. Just what you want in a testing framework, no?
httpd.requests = 0
if httpd.ready:
urllib.urlopen('http://127.0.0.1:8097/tests/wellformed/rss/aaa_wellformed.xml').read()
httpd.join(0)
if __name__ == "__main__":
runtests()

View file

@ -1 +0,0 @@
<feed><title>deflate</title></feed>

View file

@ -1 +0,0 @@
<feed><title>gzip</title></feed>

View file

@ -1 +0,0 @@
<feed xmlns="http://www.w3.org/2005/Atom"></feed>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="big5"?>
<!--
SkipUnless: __import__('codecs').lookup('big5')
Description: big5
Expect: not bozo and encoding == 'big5'
-->
<rss>
</rss>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="bogus"?>
<!--
Description: bogus encoding
Expect: bozo
-->
<rss>
</rss>

View file

@ -1,13 +0,0 @@
<?xml version="1.0"?>
<!--
Description: utf-8 interpreted as iso-8859-1 and re-encoded as utf-8
Expect: bozo and ord(entries[0]['description']) == 8230
-->
<rss version="2.0">
<channel>
<item>
<description>&acirc;&#128;&brvbar;</description>
</item>
</channel>
</rss>

View file

@ -1,10 +0,0 @@
<!--
SkipUnless: __import__('sys').version.split()[0] >= '2.2.0'
Description: crashes
Expect: 1
-->
<rss>
<item>
<description><![CDATA[<a href="http://www.example.com/">¤</a><a href="&#38;"></a>]]></description>
</item>
</rss>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Note: text/xml defaults to us-ascii, in conflict with the XML declaration of utf-8
Header: Content-type: text/xml
Description: Content-type with no charset (text/xml defaults to us-ascii)
Expect: bozo and isinstance(bozo_exception, feedparser.CharacterEncodingOverride)
-->
<feed version="0.3" xmlns="http://purl.org/atom/ns#">
<title>Iñtërnâtiônàlizætiøn</title>
</feed>

View file

@ -1,8 +0,0 @@
<?xml version="1.0"?>
<!--
Header: Content-type: text/plain
Description: text/plain + no encoding
Expect: bozo
-->
<rss version="2.0">
</rss>

View file

@ -1,8 +0,0 @@
<?xml version="1.0"?>
<!--
Header: Content-type: text/plain; charset=utf-8
Description: text/plain + charset
Expect: bozo and encoding == 'utf-8'
-->
<rss version="2.0">
</rss>

View file

@ -1,10 +0,0 @@
<!--
Description: Ensure when there are invalid bytes in encoding specified by BOM, feedparser doesn't crash
Expect: bozo
-->
<rss version="2.0">
<channel>
<title>Valid UTF8: ѨInvalid UTF8: España</title>
<description><pre class="screen"></pre></description>
</channel>
</rss

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Description: unguessable characters
Expect: bozo and entries[0].summary == u'\xe2\u20ac\u2122\xe2\u20ac\x9d\u0160'
-->
<rss version="2.0">
<channel>
<item>
<description><![CDATA[ ’<>â€<C3A2>© ]]></description>
</item>
</channel>
</rss>

Some files were not shown because too many files have changed in this diff Show more