From 9540ff3b52d1450c6f3d8f21504b1c47c58a8ad7 Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sat, 17 Oct 2015 21:40:55 +0100 Subject: [PATCH] Update TvDB API library 1.09 with changes up to (35732c9) and some pep8 and code cleanups. --- CHANGES.md | 1 + HACKS.txt | 1 + lib/tvdb_api/MANIFEST.in | 4 - lib/tvdb_api/Rakefile | 103 -- lib/tvdb_api/readme.md | 109 -- lib/tvdb_api/setup.py | 35 - lib/tvdb_api/tests/gprof2dot.py | 1638 --------------------------- lib/tvdb_api/tests/runtests.py | 28 - lib/tvdb_api/tests/test_tvdb_api.py | 577 ---------- lib/tvdb_api/tvdb_api.py | 427 ++++--- 10 files changed, 197 insertions(+), 2726 deletions(-) delete mode 100644 lib/tvdb_api/MANIFEST.in delete mode 100644 lib/tvdb_api/Rakefile delete mode 100644 lib/tvdb_api/readme.md delete mode 100644 lib/tvdb_api/setup.py delete mode 100644 lib/tvdb_api/tests/gprof2dot.py delete mode 100644 lib/tvdb_api/tests/runtests.py delete mode 100644 lib/tvdb_api/tests/test_tvdb_api.py diff --git a/CHANGES.md b/CHANGES.md index 1add630f..48402e02 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,7 @@ * Update Tornado Web Server 4.2 to 4.3.dev1 (1b6157d) * 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 TvDB API library 1.09 with changes up to (35732c9) and some pep8 and code cleanups * Fix post processing season pack folders * Fix saving torrent provider option "Seed until ratio" after recent refactor * Change white text in light theme on Manage / Episode Status Management page to black for better readability diff --git a/HACKS.txt b/HACKS.txt index c569ab61..f2b8be2c 100644 --- a/HACKS.txt +++ b/HACKS.txt @@ -7,3 +7,4 @@ Libs with customisations... /lib/requests/packages/urllib3/util/ssl_.py /tornado /lib/unrar2/unix.py +/lib/tvdb/tvdb_api.py \ No newline at end of file diff --git a/lib/tvdb_api/MANIFEST.in b/lib/tvdb_api/MANIFEST.in deleted file mode 100644 index bd227aa4..00000000 --- a/lib/tvdb_api/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include UNLICENSE -include readme.md -include tests/*.py -include Rakefile diff --git a/lib/tvdb_api/Rakefile b/lib/tvdb_api/Rakefile deleted file mode 100644 index 561deb70..00000000 --- a/lib/tvdb_api/Rakefile +++ /dev/null @@ -1,103 +0,0 @@ -require 'fileutils' - -task :default => [:clean] - -task :clean do - [".", "tests"].each do |cd| - puts "Cleaning directory #{cd}" - Dir.new(cd).each do |t| - if t =~ /.*\.pyc$/ - puts "Removing #{File.join(cd, t)}" - File.delete(File.join(cd, t)) - end - end - end -end - -desc "Upversion files" -task :upversion do - puts "Upversioning" - - Dir.glob("*.py").each do |filename| - f = File.new(filename, File::RDWR) - contents = f.read() - - contents.gsub!(/__version__ = ".+?"/){|m| - cur_version = m.scan(/\d+\.\d+/)[0].to_f - new_version = cur_version + 0.1 - - puts "Current version: #{cur_version}" - puts "New version: #{new_version}" - - new_line = "__version__ = \"#{new_version}\"" - - puts "Old line: #{m}" - puts "New line: #{new_line}" - - m = new_line - } - - puts contents[0] - - f.truncate(0) # empty the existing file - f.seek(0) - f.write(contents.to_s) # write modified file - f.close() - end -end - -desc "Upload current version to PyPi" -task :topypi => :test do - cur_file = File.open("tvdb_api.py").read() - tvdb_api_version = cur_file.scan(/__version__ = "(.*)"/) - tvdb_api_version = tvdb_api_version[0][0].to_f - - puts "Build sdist and send tvdb_api v#{tvdb_api_version} to PyPi?" - if $stdin.gets.chomp == "y" - puts "Sending source-dist (sdist) to PyPi" - - if system("python setup.py sdist register upload") - puts "tvdb_api uploaded!" - end - - else - puts "Cancelled" - end -end - -desc "Profile by running unittests" -task :profile do - cd "tests" - puts "Profiling.." - `python -m cProfile -o prof_runtest.prof runtests.py` - puts "Converting prof to dot" - `python gprof2dot.py -o prof_runtest.dot -f pstats prof_runtest.prof` - puts "Generating graph" - `~/Applications/dev/graphviz.app/Contents/macOS/dot -Tpng -o profile.png prof_runtest.dot -Gbgcolor=black` - puts "Cleanup" - rm "prof_runtest.dot" - rm "prof_runtest.prof" -end - -task :test do - puts "Nosetest'ing" - if not system("nosetests -v --with-doctest") - raise "Test failed!" - end - - puts "Doctesting *.py (excluding setup.py)" - Dir.glob("*.py").select{|e| ! e.match(/setup.py/)}.each do |filename| - if filename =~ /^setup\.py/ - skip - end - puts "Doctesting #{filename}" - if not system("python", "-m", "doctest", filename) - raise "Failed doctest" - end - end - - puts "Doctesting readme.md" - if not system("python", "-m", "doctest", "readme.md") - raise "Doctest" - end -end diff --git a/lib/tvdb_api/readme.md b/lib/tvdb_api/readme.md deleted file mode 100644 index b0b0cfdf..00000000 --- a/lib/tvdb_api/readme.md +++ /dev/null @@ -1,109 +0,0 @@ -# `tvdb_api` - -`tvdb_api` is an easy to use interface to [thetvdb.com][tvdb] - -`tvnamer` has moved to a separate repository: [github.com/dbr/tvnamer][tvnamer] - it is a utility which uses `tvdb_api` to rename files from `some.show.s01e03.blah.abc.avi` to `Some Show - [01x03] - The Episode Name.avi` (which works by getting the episode name from `tvdb_api`) - -[![Build Status](https://secure.travis-ci.org/dbr/tvdb_api.png?branch=master)](http://travis-ci.org/dbr/tvdb_api) - -## To install - -You can easily install `tvdb_api` via `easy_install` - - easy_install tvdb_api - -You may need to use sudo, depending on your setup: - - sudo easy_install tvdb_api - -The [`tvnamer`][tvnamer] command-line tool can also be installed via `easy_install`, this installs `tvdb_api` as a dependancy: - - easy_install tvnamer - - -## Basic usage - - import tvdb_api - t = indexerApi() - episode = t['My Name Is Earl'][1][3] # get season 1, episode 3 of show - print episode['episodename'] # Print episode name - -## Advanced usage - -Most of the documentation is in docstrings. The examples are tested (using doctest) so will always be up to date and working. - -The docstring for `Tvdb.__init__` lists all initialisation arguments, including support for non-English searches, custom "Select Series" interfaces and enabling the retrieval of banners and extended actor information. You can also override the default API key using `apikey`, recommended if you're using `tvdb_api` in a larger script or application - -### Exceptions - -There are several exceptions you may catch, these can be imported from `tvdb_api`: - -- `tvdb_error` - this is raised when there is an error communicating with [thetvdb.com][tvdb] (a network error most commonly) -- `tvdb_userabort` - raised when a user aborts the Select Series dialog (by `ctrl+c`, or entering `q`) -- `tvdb_shownotfound` - raised when `t['show name']` cannot find anything -- `tvdb_seasonnotfound` - raised when the requested series (`t['show name][99]`) does not exist -- `tvdb_episodenotfound` - raised when the requested episode (`t['show name][1][99]`) does not exist. -- `tvdb_attributenotfound` - raised when the requested attribute is not found (`t['show name']['an attribute']`, `t['show name'][1]['an attribute']`, or ``t['show name'][1][1]['an attribute']``) - -### Series data - -All data exposed by [thetvdb.com][tvdb] is accessible via the `Show` class. A Show is retrieved by doing.. - - >>> import tvdb_api - >>> t = indexerApi() - >>> show = t['scrubs'] - >>> type(show) - - -For example, to find out what network Scrubs is aired: - - >>> t['scrubs']['network'] - u'ABC' - -The data is stored in an attribute named `data`, within the Show instance: - - >>> t['scrubs'].data.keys() - ['networkid', 'rating', 'airs_dayofweek', 'contentrating', 'seriesname', 'id', 'airs_time', 'network', 'fanart', 'lastupdated', 'actors', 'ratingcount', 'status', 'added', 'poster', 'imdb_id', 'genre', 'banner', 'seriesid', 'language', 'zap2it_id', 'addedby', 'tms_wanted', 'firstaired', 'runtime', 'overview'] - -Although each element is also accessible via `t['scrubs']` for ease-of-use: - - >>> t['scrubs']['rating'] - u'9.0' - -This is the recommended way of retrieving "one-off" data (for example, if you are only interested in "seriesname"). If you wish to iterate over all data, or check if a particular show has a specific piece of data, use the `data` attribute, - - >>> 'rating' in t['scrubs'].data - True - -### Banners and actors - -Since banners and actors are separate XML files, retrieving them by default is undesirable. If you wish to retrieve banners (and other fanart), use the `banners` Tvdb initialisation argument: - - >>> from tvdb_api import Tvdb - >>> t = Tvdb(banners = True) - -Then access the data using a `Show`'s `_banner` key: - - >>> t['scrubs']['_banners'].keys() - ['fanart', 'poster', 'series', 'season'] - -The banner data structure will be improved in future versions. - -Extended actor data is accessible similarly: - - >>> t = Tvdb(actors = True) - >>> actors = t['scrubs']['_actors'] - >>> actors[0] - - >>> actors[0].keys() - ['sortorder', 'image', 'role', 'id', 'name'] - >>> actors[0]['role'] - u'Dr. John Michael "J.D." Dorian' - -Remember a simple list of actors is accessible via the default Show data: - - >>> t['scrubs']['actors'] - u'|Zach Braff|Donald Faison|Sarah Chalke|Christa Miller|Aloma Wright|Robert Maschio|Sam Lloyd|Neil Flynn|Ken Jenkins|Judy Reyes|John C. McGinley|Travis Schuldt|Johnny Kastl|Heather Graham|Michael Mosley|Kerry Bish\xe9|Dave Franco|Eliza Coupe|' - -[tvdb]: http://thetvdb.com -[tvnamer]: http://github.com/dbr/tvnamer diff --git a/lib/tvdb_api/setup.py b/lib/tvdb_api/setup.py deleted file mode 100644 index e4d66801..00000000 --- a/lib/tvdb_api/setup.py +++ /dev/null @@ -1,35 +0,0 @@ -from setuptools import setup -setup( -name = 'tvdb_api', -version='1.9', - -author='dbr/Ben', -description='Interface to thetvdb.com', -url='http://github.com/dbr/tvdb_api/tree/master', -license='unlicense', - -long_description="""\ -An easy to use API interface to TheTVDB.com -Basic usage is: - ->>> import tvdb_api ->>> t = tvdb_api.Tvdb() ->>> ep = t['My Name Is Earl'][1][22] ->>> ep - ->>> ep['episodename'] -u'Stole a Badge' -""", - -py_modules = ['tvdb_api', 'tvdb_ui', 'tvdb_exceptions', 'tvdb_cache'], - -classifiers=[ - "Intended Audience :: Developers", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Topic :: Multimedia", - "Topic :: Utilities", - "Topic :: Software Development :: Libraries :: Python Modules", -] -) diff --git a/lib/tvdb_api/tests/gprof2dot.py b/lib/tvdb_api/tests/gprof2dot.py deleted file mode 100644 index 20a5a50a..00000000 --- a/lib/tvdb_api/tests/gprof2dot.py +++ /dev/null @@ -1,1638 +0,0 @@ -#!/usr/bin/env python2 -# -# Copyright 2008 Jose Fonseca -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# - -"""Generate a dot graph from the output of several profilers.""" - -__author__ = "Jose Fonseca" - -__version__ = "1.0" - - -import sys -import math -import os.path -import re -import textwrap -import optparse - - -try: - # Debugging helper module - import debug -except ImportError: - pass - - -def percentage(p): - return "%.02f%%" % (p*100.0,) - -def add(a, b): - return a + b - -def equal(a, b): - if a == b: - return a - else: - return None - -def fail(a, b): - assert False - - -def ratio(numerator, denominator): - numerator = float(numerator) - denominator = float(denominator) - assert 0.0 <= numerator - assert numerator <= denominator - try: - return numerator/denominator - except ZeroDivisionError: - # 0/0 is undefined, but 1.0 yields more useful results - return 1.0 - - -class UndefinedEvent(Exception): - """Raised when attempting to get an event which is undefined.""" - - def __init__(self, event): - Exception.__init__(self) - self.event = event - - def __str__(self): - return 'unspecified event %s' % self.event.name - - -class Event(object): - """Describe a kind of event, and its basic operations.""" - - def __init__(self, name, null, aggregator, formatter = str): - self.name = name - self._null = null - self._aggregator = aggregator - self._formatter = formatter - - def __eq__(self, other): - return self is other - - def __hash__(self): - return id(self) - - def null(self): - return self._null - - def aggregate(self, val1, val2): - """Aggregate two event values.""" - assert val1 is not None - assert val2 is not None - return self._aggregator(val1, val2) - - def format(self, val): - """Format an event value.""" - assert val is not None - return self._formatter(val) - - -MODULE = Event("Module", None, equal) -PROCESS = Event("Process", None, equal) - -CALLS = Event("Calls", 0, add) -SAMPLES = Event("Samples", 0, add) - -TIME = Event("Time", 0.0, add, lambda x: '(' + str(x) + ')') -TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')') -TOTAL_TIME = Event("Total time", 0.0, fail) -TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage) - -CALL_RATIO = Event("Call ratio", 0.0, add, percentage) - -PRUNE_RATIO = Event("Prune ratio", 0.0, add, percentage) - - -class Object(object): - """Base class for all objects in profile which can store events.""" - - def __init__(self, events=None): - if events is None: - self.events = {} - else: - self.events = events - - def __hash__(self): - return id(self) - - def __eq__(self, other): - return self is other - - def __contains__(self, event): - return event in self.events - - def __getitem__(self, event): - try: - return self.events[event] - except KeyError: - raise UndefinedEvent(event) - - def __setitem__(self, event, value): - if value is None: - if event in self.events: - del self.events[event] - else: - self.events[event] = value - - -class Call(Object): - """A call between functions. - - There should be at most one call object for every pair of functions. - """ - - def __init__(self, callee_id): - Object.__init__(self) - self.callee_id = callee_id - - -class Function(Object): - """A function.""" - - def __init__(self, id, name): - Object.__init__(self) - self.id = id - self.name = name - self.calls = {} - self.cycle = None - - def add_call(self, call): - if call.callee_id in self.calls: - sys.stderr.write('warning: overwriting call from function %s to %s\n' % (str(self.id), str(call.callee_id))) - self.calls[call.callee_id] = call - - # TODO: write utility functions - - def __repr__(self): - return self.name - - -class Cycle(Object): - """A cycle made from recursive function calls.""" - - def __init__(self): - Object.__init__(self) - # XXX: Do cycles need an id? - self.functions = set() - - def add_function(self, function): - assert function not in self.functions - self.functions.add(function) - # XXX: Aggregate events? - if function.cycle is not None: - for other in function.cycle.functions: - if function not in self.functions: - self.add_function(other) - function.cycle = self - - -class Profile(Object): - """The whole profile.""" - - def __init__(self): - Object.__init__(self) - self.functions = {} - self.cycles = [] - - def add_function(self, function): - if function.id in self.functions: - sys.stderr.write('warning: overwriting function %s (id %s)\n' % (function.name, str(function.id))) - self.functions[function.id] = function - - def add_cycle(self, cycle): - self.cycles.append(cycle) - - def validate(self): - """Validate the edges.""" - - for function in self.functions.itervalues(): - for callee_id in function.calls.keys(): - assert function.calls[callee_id].callee_id == callee_id - if callee_id not in self.functions: - sys.stderr.write('warning: call to undefined function %s from function %s\n' % (str(callee_id), function.name)) - del function.calls[callee_id] - - def find_cycles(self): - """Find cycles using Tarjan's strongly connected components algorithm.""" - - # Apply the Tarjan's algorithm successively until all functions are visited - visited = set() - for function in self.functions.itervalues(): - if function not in visited: - self._tarjan(function, 0, [], {}, {}, visited) - cycles = [] - for function in self.functions.itervalues(): - if function.cycle is not None and function.cycle not in cycles: - cycles.append(function.cycle) - self.cycles = cycles - if 0: - for cycle in cycles: - sys.stderr.write("Cycle:\n") - for member in cycle.functions: - sys.stderr.write("\t%s\n" % member.name) - - def _tarjan(self, function, order, stack, orders, lowlinks, visited): - """Tarjan's strongly connected components algorithm. - - See also: - - http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm - """ - - visited.add(function) - orders[function] = order - lowlinks[function] = order - order += 1 - pos = len(stack) - stack.append(function) - for call in function.calls.itervalues(): - callee = self.functions[call.callee_id] - # TODO: use a set to optimize lookup - if callee not in orders: - order = self._tarjan(callee, order, stack, orders, lowlinks, visited) - lowlinks[function] = min(lowlinks[function], lowlinks[callee]) - elif callee in stack: - lowlinks[function] = min(lowlinks[function], orders[callee]) - if lowlinks[function] == orders[function]: - # Strongly connected component found - members = stack[pos:] - del stack[pos:] - if len(members) > 1: - cycle = Cycle() - for member in members: - cycle.add_function(member) - return order - - def call_ratios(self, event): - # Aggregate for incoming calls - cycle_totals = {} - for cycle in self.cycles: - cycle_totals[cycle] = 0.0 - function_totals = {} - for function in self.functions.itervalues(): - function_totals[function] = 0.0 - for function in self.functions.itervalues(): - for call in function.calls.itervalues(): - if call.callee_id != function.id: - callee = self.functions[call.callee_id] - function_totals[callee] += call[event] - if callee.cycle is not None and callee.cycle is not function.cycle: - cycle_totals[callee.cycle] += call[event] - - # Compute the ratios - for function in self.functions.itervalues(): - for call in function.calls.itervalues(): - assert CALL_RATIO not in call - if call.callee_id != function.id: - callee = self.functions[call.callee_id] - if callee.cycle is not None and callee.cycle is not function.cycle: - total = cycle_totals[callee.cycle] - else: - total = function_totals[callee] - call[CALL_RATIO] = ratio(call[event], total) - - def integrate(self, outevent, inevent): - """Propagate function time ratio allong the function calls. - - Must be called after finding the cycles. - - See also: - - http://citeseer.ist.psu.edu/graham82gprof.html - """ - - # Sanity checking - assert outevent not in self - for function in self.functions.itervalues(): - assert outevent not in function - assert inevent in function - for call in function.calls.itervalues(): - assert outevent not in call - if call.callee_id != function.id: - assert CALL_RATIO in call - - # Aggregate the input for each cycle - for cycle in self.cycles: - total = inevent.null() - for function in self.functions.itervalues(): - total = inevent.aggregate(total, function[inevent]) - self[inevent] = total - - # Integrate along the edges - total = inevent.null() - for function in self.functions.itervalues(): - total = inevent.aggregate(total, function[inevent]) - self._integrate_function(function, outevent, inevent) - self[outevent] = total - - def _integrate_function(self, function, outevent, inevent): - if function.cycle is not None: - return self._integrate_cycle(function.cycle, outevent, inevent) - else: - if outevent not in function: - total = function[inevent] - for call in function.calls.itervalues(): - if call.callee_id != function.id: - total += self._integrate_call(call, outevent, inevent) - function[outevent] = total - return function[outevent] - - def _integrate_call(self, call, outevent, inevent): - assert outevent not in call - assert CALL_RATIO in call - callee = self.functions[call.callee_id] - subtotal = call[CALL_RATIO]*self._integrate_function(callee, outevent, inevent) - call[outevent] = subtotal - return subtotal - - def _integrate_cycle(self, cycle, outevent, inevent): - if outevent not in cycle: - - total = inevent.null() - for member in cycle.functions: - subtotal = member[inevent] - for call in member.calls.itervalues(): - callee = self.functions[call.callee_id] - if callee.cycle is not cycle: - subtotal += self._integrate_call(call, outevent, inevent) - total += subtotal - cycle[outevent] = total - - callees = {} - for function in self.functions.itervalues(): - if function.cycle is not cycle: - for call in function.calls.itervalues(): - callee = self.functions[call.callee_id] - if callee.cycle is cycle: - try: - callees[callee] += call[CALL_RATIO] - except KeyError: - callees[callee] = call[CALL_RATIO] - - for callee, call_ratio in callees.iteritems(): - ranks = {} - call_ratios = {} - partials = {} - self._rank_cycle_function(cycle, callee, 0, ranks) - self._call_ratios_cycle(cycle, callee, ranks, call_ratios, set()) - partial = self._integrate_cycle_function(cycle, callee, call_ratio, partials, ranks, call_ratios, outevent, inevent) - assert partial == max(partials.values()) - assert not total or abs(1.0 - partial/(call_ratio*total)) <= 0.001 - - return cycle[outevent] - - def _rank_cycle_function(self, cycle, function, rank, ranks): - if function not in ranks or ranks[function] > rank: - ranks[function] = rank - for call in function.calls.itervalues(): - if call.callee_id != function.id: - callee = self.functions[call.callee_id] - if callee.cycle is cycle: - self._rank_cycle_function(cycle, callee, rank + 1, ranks) - - def _call_ratios_cycle(self, cycle, function, ranks, call_ratios, visited): - if function not in visited: - visited.add(function) - for call in function.calls.itervalues(): - if call.callee_id != function.id: - callee = self.functions[call.callee_id] - if callee.cycle is cycle: - if ranks[callee] > ranks[function]: - call_ratios[callee] = call_ratios.get(callee, 0.0) + call[CALL_RATIO] - self._call_ratios_cycle(cycle, callee, ranks, call_ratios, visited) - - def _integrate_cycle_function(self, cycle, function, partial_ratio, partials, ranks, call_ratios, outevent, inevent): - if function not in partials: - partial = partial_ratio*function[inevent] - for call in function.calls.itervalues(): - if call.callee_id != function.id: - callee = self.functions[call.callee_id] - if callee.cycle is not cycle: - assert outevent in call - partial += partial_ratio*call[outevent] - else: - if ranks[callee] > ranks[function]: - callee_partial = self._integrate_cycle_function(cycle, callee, partial_ratio, partials, ranks, call_ratios, outevent, inevent) - call_ratio = ratio(call[CALL_RATIO], call_ratios[callee]) - call_partial = call_ratio*callee_partial - try: - call[outevent] += call_partial - except UndefinedEvent: - call[outevent] = call_partial - partial += call_partial - partials[function] = partial - try: - function[outevent] += partial - except UndefinedEvent: - function[outevent] = partial - return partials[function] - - def aggregate(self, event): - """Aggregate an event for the whole profile.""" - - total = event.null() - for function in self.functions.itervalues(): - try: - total = event.aggregate(total, function[event]) - except UndefinedEvent: - return - self[event] = total - - def ratio(self, outevent, inevent): - assert outevent not in self - assert inevent in self - for function in self.functions.itervalues(): - assert outevent not in function - assert inevent in function - function[outevent] = ratio(function[inevent], self[inevent]) - for call in function.calls.itervalues(): - assert outevent not in call - if inevent in call: - call[outevent] = ratio(call[inevent], self[inevent]) - self[outevent] = 1.0 - - def prune(self, node_thres, edge_thres): - """Prune the profile""" - - # compute the prune ratios - for function in self.functions.itervalues(): - try: - function[PRUNE_RATIO] = function[TOTAL_TIME_RATIO] - except UndefinedEvent: - pass - - for call in function.calls.itervalues(): - callee = self.functions[call.callee_id] - - if TOTAL_TIME_RATIO in call: - # handle exact cases first - call[PRUNE_RATIO] = call[TOTAL_TIME_RATIO] - else: - try: - # make a safe estimate - call[PRUNE_RATIO] = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO]) - except UndefinedEvent: - pass - - # prune the nodes - for function_id in self.functions.keys(): - function = self.functions[function_id] - try: - if function[PRUNE_RATIO] < node_thres: - del self.functions[function_id] - except UndefinedEvent: - pass - - # prune the egdes - for function in self.functions.itervalues(): - for callee_id in function.calls.keys(): - call = function.calls[callee_id] - try: - if callee_id not in self.functions or call[PRUNE_RATIO] < edge_thres: - del function.calls[callee_id] - except UndefinedEvent: - pass - - def dump(self): - for function in self.functions.itervalues(): - sys.stderr.write('Function %s:\n' % (function.name,)) - self._dump_events(function.events) - for call in function.calls.itervalues(): - callee = self.functions[call.callee_id] - sys.stderr.write(' Call %s:\n' % (callee.name,)) - self._dump_events(call.events) - - def _dump_events(self, events): - for event, value in events.iteritems(): - sys.stderr.write(' %s: %s\n' % (event.name, event.format(value))) - - -class Struct: - """Masquerade a dictionary with a structure-like behavior.""" - - def __init__(self, attrs = None): - if attrs is None: - attrs = {} - self.__dict__['_attrs'] = attrs - - def __getattr__(self, name): - try: - return self._attrs[name] - except KeyError: - raise AttributeError(name) - - def __setattr__(self, name, value): - self._attrs[name] = value - - def __str__(self): - return str(self._attrs) - - def __repr__(self): - return repr(self._attrs) - - -class ParseError(Exception): - """Raised when parsing to signal mismatches.""" - - def __init__(self, msg, line): - self.msg = msg - # TODO: store more source line information - self.line = line - - def __str__(self): - return '%s: %r' % (self.msg, self.line) - - -class Parser: - """Parser interface.""" - - def __init__(self): - pass - - def parse(self): - raise NotImplementedError - - -class LineParser(Parser): - """Base class for parsers that read line-based formats.""" - - def __init__(self, file): - Parser.__init__(self) - self._file = file - self.__line = None - self.__eof = False - - def readline(self): - line = self._file.readline() - if not line: - self.__line = '' - self.__eof = True - self.__line = line.rstrip('\r\n') - - def lookahead(self): - assert self.__line is not None - return self.__line - - def consume(self): - assert self.__line is not None - line = self.__line - self.readline() - return line - - def eof(self): - assert self.__line is not None - return self.__eof - - -class GprofParser(Parser): - """Parser for GNU gprof output. - - See also: - - Chapter "Interpreting gprof's Output" from the GNU gprof manual - http://sourceware.org/binutils/docs-2.18/gprof/Call-Graph.html#Call-Graph - - File "cg_print.c" from the GNU gprof source code - http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/src/gprof/cg_print.c?rev=1.12&cvsroot=src - """ - - def __init__(self, fp): - Parser.__init__(self) - self.fp = fp - self.functions = {} - self.cycles = {} - - def readline(self): - line = self.fp.readline() - if not line: - sys.stderr.write('error: unexpected end of file\n') - sys.exit(1) - line = line.rstrip('\r\n') - return line - - _int_re = re.compile(r'^\d+$') - _float_re = re.compile(r'^\d+\.\d+$') - - def translate(self, mo): - """Extract a structure from a match object, while translating the types in the process.""" - attrs = {} - groupdict = mo.groupdict() - for name, value in groupdict.iteritems(): - if value is None: - value = None - elif self._int_re.match(value): - value = int(value) - elif self._float_re.match(value): - value = float(value) - attrs[name] = (value) - return Struct(attrs) - - _cg_header_re = re.compile( - # original gprof header - r'^\s+called/total\s+parents\s*$|' + - r'^index\s+%time\s+self\s+descendents\s+called\+self\s+name\s+index\s*$|' + - r'^\s+called/total\s+children\s*$|' + - # GNU gprof header - r'^index\s+%\s+time\s+self\s+children\s+called\s+name\s*$' - ) - - _cg_ignore_re = re.compile( - # spontaneous - r'^\s+\s*$|' - # internal calls (such as "mcount") - r'^.*\((\d+)\)$' - ) - - _cg_primary_re = re.compile( - r'^\[(?P\d+)\]' + - r'\s+(?P\d+\.\d+)' + - r'\s+(?P\d+\.\d+)' + - r'\s+(?P\d+\.\d+)' + - r'\s+(?:(?P\d+)(?:\+(?P\d+))?)?' + - r'\s+(?P\S.*?)' + - r'(?:\s+\d+)>)?' + - r'\s\[(\d+)\]$' - ) - - _cg_parent_re = re.compile( - r'^\s+(?P\d+\.\d+)?' + - r'\s+(?P\d+\.\d+)?' + - r'\s+(?P\d+)(?:/(?P\d+))?' + - r'\s+(?P\S.*?)' + - r'(?:\s+\d+)>)?' + - r'\s\[(?P\d+)\]$' - ) - - _cg_child_re = _cg_parent_re - - _cg_cycle_header_re = re.compile( - r'^\[(?P\d+)\]' + - r'\s+(?P\d+\.\d+)' + - r'\s+(?P\d+\.\d+)' + - r'\s+(?P\d+\.\d+)' + - r'\s+(?:(?P\d+)(?:\+(?P\d+))?)?' + - r'\s+\d+)\sas\sa\swhole>' + - r'\s\[(\d+)\]$' - ) - - _cg_cycle_member_re = re.compile( - r'^\s+(?P\d+\.\d+)?' + - r'\s+(?P\d+\.\d+)?' + - r'\s+(?P\d+)(?:\+(?P\d+))?' + - r'\s+(?P\S.*?)' + - r'(?:\s+\d+)>)?' + - r'\s\[(?P\d+)\]$' - ) - - _cg_sep_re = re.compile(r'^--+$') - - def parse_function_entry(self, lines): - parents = [] - children = [] - - while True: - if not lines: - sys.stderr.write('warning: unexpected end of entry\n') - line = lines.pop(0) - if line.startswith('['): - break - - # read function parent line - mo = self._cg_parent_re.match(line) - if not mo: - if self._cg_ignore_re.match(line): - continue - sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) - else: - parent = self.translate(mo) - parents.append(parent) - - # read primary line - mo = self._cg_primary_re.match(line) - if not mo: - sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) - return - else: - function = self.translate(mo) - - while lines: - line = lines.pop(0) - - # read function subroutine line - mo = self._cg_child_re.match(line) - if not mo: - if self._cg_ignore_re.match(line): - continue - sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) - else: - child = self.translate(mo) - children.append(child) - - function.parents = parents - function.children = children - - self.functions[function.index] = function - - def parse_cycle_entry(self, lines): - - # read cycle header line - line = lines[0] - mo = self._cg_cycle_header_re.match(line) - if not mo: - sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) - return - cycle = self.translate(mo) - - # read cycle member lines - cycle.functions = [] - for line in lines[1:]: - mo = self._cg_cycle_member_re.match(line) - if not mo: - sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) - continue - call = self.translate(mo) - cycle.functions.append(call) - - self.cycles[cycle.cycle] = cycle - - def parse_cg_entry(self, lines): - if lines[0].startswith("["): - self.parse_cycle_entry(lines) - else: - self.parse_function_entry(lines) - - def parse_cg(self): - """Parse the call graph.""" - - # skip call graph header - while not self._cg_header_re.match(self.readline()): - pass - line = self.readline() - while self._cg_header_re.match(line): - line = self.readline() - - # process call graph entries - entry_lines = [] - while line != '\014': # form feed - if line and not line.isspace(): - if self._cg_sep_re.match(line): - self.parse_cg_entry(entry_lines) - entry_lines = [] - else: - entry_lines.append(line) - line = self.readline() - - def parse(self): - self.parse_cg() - self.fp.close() - - profile = Profile() - profile[TIME] = 0.0 - - cycles = {} - for index in self.cycles.iterkeys(): - cycles[index] = Cycle() - - for entry in self.functions.itervalues(): - # populate the function - function = Function(entry.index, entry.name) - function[TIME] = entry.self - if entry.called is not None: - function[CALLS] = entry.called - if entry.called_self is not None: - call = Call(entry.index) - call[CALLS] = entry.called_self - function[CALLS] += entry.called_self - - # populate the function calls - for child in entry.children: - call = Call(child.index) - - assert child.called is not None - call[CALLS] = child.called - - if child.index not in self.functions: - # NOTE: functions that were never called but were discovered by gprof's - # static call graph analysis dont have a call graph entry so we need - # to add them here - missing = Function(child.index, child.name) - function[TIME] = 0.0 - function[CALLS] = 0 - profile.add_function(missing) - - function.add_call(call) - - profile.add_function(function) - - if entry.cycle is not None: - cycles[entry.cycle].add_function(function) - - profile[TIME] = profile[TIME] + function[TIME] - - for cycle in cycles.itervalues(): - profile.add_cycle(cycle) - - # Compute derived events - profile.validate() - profile.ratio(TIME_RATIO, TIME) - profile.call_ratios(CALLS) - profile.integrate(TOTAL_TIME, TIME) - profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME) - - return profile - - -class OprofileParser(LineParser): - """Parser for oprofile callgraph output. - - See also: - - http://oprofile.sourceforge.net/doc/opreport.html#opreport-callgraph - """ - - _fields_re = { - 'samples': r'(?P\d+)', - '%': r'(?P\S+)', - 'linenr info': r'(?P\(no location information\)|\S+:\d+)', - 'image name': r'(?P\S+(?:\s\(tgid:[^)]*\))?)', - 'app name': r'(?P\S+)', - 'symbol name': r'(?P\(no symbols\)|.+?)', - } - - def __init__(self, infile): - LineParser.__init__(self, infile) - self.entries = {} - self.entry_re = None - - def add_entry(self, callers, function, callees): - try: - entry = self.entries[function.id] - except KeyError: - self.entries[function.id] = (callers, function, callees) - else: - callers_total, function_total, callees_total = entry - self.update_subentries_dict(callers_total, callers) - function_total.samples += function.samples - self.update_subentries_dict(callees_total, callees) - - def update_subentries_dict(self, totals, partials): - for partial in partials.itervalues(): - try: - total = totals[partial.id] - except KeyError: - totals[partial.id] = partial - else: - total.samples += partial.samples - - def parse(self): - # read lookahead - self.readline() - - self.parse_header() - while self.lookahead(): - self.parse_entry() - - profile = Profile() - - reverse_call_samples = {} - - # populate the profile - profile[SAMPLES] = 0 - for _callers, _function, _callees in self.entries.itervalues(): - function = Function(_function.id, _function.name) - function[SAMPLES] = _function.samples - profile.add_function(function) - profile[SAMPLES] += _function.samples - - if _function.application: - function[PROCESS] = os.path.basename(_function.application) - if _function.image: - function[MODULE] = os.path.basename(_function.image) - - total_callee_samples = 0 - for _callee in _callees.itervalues(): - total_callee_samples += _callee.samples - - for _callee in _callees.itervalues(): - if not _callee.self: - call = Call(_callee.id) - call[SAMPLES] = _callee.samples - function.add_call(call) - - # compute derived data - profile.validate() - profile.find_cycles() - profile.ratio(TIME_RATIO, SAMPLES) - profile.call_ratios(SAMPLES) - profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) - - return profile - - def parse_header(self): - while not self.match_header(): - self.consume() - line = self.lookahead() - fields = re.split(r'\s\s+', line) - entry_re = r'^\s*' + r'\s+'.join([self._fields_re[field] for field in fields]) + r'(?P\s+\[self\])?$' - self.entry_re = re.compile(entry_re) - self.skip_separator() - - def parse_entry(self): - callers = self.parse_subentries() - if self.match_primary(): - function = self.parse_subentry() - if function is not None: - callees = self.parse_subentries() - self.add_entry(callers, function, callees) - self.skip_separator() - - def parse_subentries(self): - subentries = {} - while self.match_secondary(): - subentry = self.parse_subentry() - subentries[subentry.id] = subentry - return subentries - - def parse_subentry(self): - entry = Struct() - line = self.consume() - mo = self.entry_re.match(line) - if not mo: - raise ParseError('failed to parse', line) - fields = mo.groupdict() - entry.samples = int(fields.get('samples', 0)) - entry.percentage = float(fields.get('percentage', 0.0)) - if 'source' in fields and fields['source'] != '(no location information)': - source = fields['source'] - filename, lineno = source.split(':') - entry.filename = filename - entry.lineno = int(lineno) - else: - source = '' - entry.filename = None - entry.lineno = None - entry.image = fields.get('image', '') - entry.application = fields.get('application', '') - if 'symbol' in fields and fields['symbol'] != '(no symbols)': - entry.symbol = fields['symbol'] - else: - entry.symbol = '' - if entry.symbol.startswith('"') and entry.symbol.endswith('"'): - entry.symbol = entry.symbol[1:-1] - entry.id = ':'.join((entry.application, entry.image, source, entry.symbol)) - entry.self = fields.get('self', None) != None - if entry.self: - entry.id += ':self' - if entry.symbol: - entry.name = entry.symbol - else: - entry.name = entry.image - return entry - - def skip_separator(self): - while not self.match_separator(): - self.consume() - self.consume() - - def match_header(self): - line = self.lookahead() - return line.startswith('samples') - - def match_separator(self): - line = self.lookahead() - return line == '-'*len(line) - - def match_primary(self): - line = self.lookahead() - return not line[:1].isspace() - - def match_secondary(self): - line = self.lookahead() - return line[:1].isspace() - - -class SharkParser(LineParser): - """Parser for MacOSX Shark output. - - Author: tom@dbservice.com - """ - - def __init__(self, infile): - LineParser.__init__(self, infile) - self.stack = [] - self.entries = {} - - def add_entry(self, function): - try: - entry = self.entries[function.id] - except KeyError: - self.entries[function.id] = (function, { }) - else: - function_total, callees_total = entry - function_total.samples += function.samples - - def add_callee(self, function, callee): - func, callees = self.entries[function.id] - try: - entry = callees[callee.id] - except KeyError: - callees[callee.id] = callee - else: - entry.samples += callee.samples - - def parse(self): - self.readline() - self.readline() - self.readline() - self.readline() - - match = re.compile(r'(?P[|+ ]*)(?P\d+), (?P[^,]+), (?P.*)') - - while self.lookahead(): - line = self.consume() - mo = match.match(line) - if not mo: - raise ParseError('failed to parse', line) - - fields = mo.groupdict() - prefix = len(fields.get('prefix', 0)) / 2 - 1 - - symbol = str(fields.get('symbol', 0)) - image = str(fields.get('image', 0)) - - entry = Struct() - entry.id = ':'.join([symbol, image]) - entry.samples = int(fields.get('samples', 0)) - - entry.name = symbol - entry.image = image - - # adjust the callstack - if prefix < len(self.stack): - del self.stack[prefix:] - - if prefix == len(self.stack): - self.stack.append(entry) - - # if the callstack has had an entry, it's this functions caller - if prefix > 0: - self.add_callee(self.stack[prefix - 1], entry) - - self.add_entry(entry) - - profile = Profile() - profile[SAMPLES] = 0 - for _function, _callees in self.entries.itervalues(): - function = Function(_function.id, _function.name) - function[SAMPLES] = _function.samples - profile.add_function(function) - profile[SAMPLES] += _function.samples - - if _function.image: - function[MODULE] = os.path.basename(_function.image) - - for _callee in _callees.itervalues(): - call = Call(_callee.id) - call[SAMPLES] = _callee.samples - function.add_call(call) - - # compute derived data - profile.validate() - profile.find_cycles() - profile.ratio(TIME_RATIO, SAMPLES) - profile.call_ratios(SAMPLES) - profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) - - return profile - - -class PstatsParser: - """Parser python profiling statistics saved with te pstats module.""" - - def __init__(self, *filename): - import pstats - self.stats = pstats.Stats(*filename) - self.profile = Profile() - self.function_ids = {} - - def get_function_name(self, (filename, line, name)): - module = os.path.splitext(filename)[0] - module = os.path.basename(module) - return "%s:%d:%s" % (module, line, name) - - def get_function(self, key): - try: - id = self.function_ids[key] - except KeyError: - id = len(self.function_ids) - name = self.get_function_name(key) - function = Function(id, name) - self.profile.functions[id] = function - self.function_ids[key] = id - else: - function = self.profile.functions[id] - return function - - def parse(self): - self.profile[TIME] = 0.0 - self.profile[TOTAL_TIME] = self.stats.total_tt - for fn, (cc, nc, tt, ct, callers) in self.stats.stats.iteritems(): - callee = self.get_function(fn) - callee[CALLS] = nc - callee[TOTAL_TIME] = ct - callee[TIME] = tt - self.profile[TIME] += tt - self.profile[TOTAL_TIME] = max(self.profile[TOTAL_TIME], ct) - for fn, value in callers.iteritems(): - caller = self.get_function(fn) - call = Call(callee.id) - if isinstance(value, tuple): - for i in xrange(0, len(value), 4): - nc, cc, tt, ct = value[i:i+4] - if CALLS in call: - call[CALLS] += cc - else: - call[CALLS] = cc - - if TOTAL_TIME in call: - call[TOTAL_TIME] += ct - else: - call[TOTAL_TIME] = ct - - else: - call[CALLS] = value - call[TOTAL_TIME] = ratio(value, nc)*ct - - caller.add_call(call) - #self.stats.print_stats() - #self.stats.print_callees() - - # Compute derived events - self.profile.validate() - self.profile.ratio(TIME_RATIO, TIME) - self.profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME) - - return self.profile - - -class Theme: - - def __init__(self, - bgcolor = (0.0, 0.0, 1.0), - mincolor = (0.0, 0.0, 0.0), - maxcolor = (0.0, 0.0, 1.0), - fontname = "Arial", - minfontsize = 10.0, - maxfontsize = 10.0, - minpenwidth = 0.5, - maxpenwidth = 4.0, - gamma = 2.2): - self.bgcolor = bgcolor - self.mincolor = mincolor - self.maxcolor = maxcolor - self.fontname = fontname - self.minfontsize = minfontsize - self.maxfontsize = maxfontsize - self.minpenwidth = minpenwidth - self.maxpenwidth = maxpenwidth - self.gamma = gamma - - def graph_bgcolor(self): - return self.hsl_to_rgb(*self.bgcolor) - - def graph_fontname(self): - return self.fontname - - def graph_fontsize(self): - return self.minfontsize - - def node_bgcolor(self, weight): - return self.color(weight) - - def node_fgcolor(self, weight): - return self.graph_bgcolor() - - def node_fontsize(self, weight): - return self.fontsize(weight) - - def edge_color(self, weight): - return self.color(weight) - - def edge_fontsize(self, weight): - return self.fontsize(weight) - - def edge_penwidth(self, weight): - return max(weight*self.maxpenwidth, self.minpenwidth) - - def edge_arrowsize(self, weight): - return 0.5 * math.sqrt(self.edge_penwidth(weight)) - - def fontsize(self, weight): - return max(weight**2 * self.maxfontsize, self.minfontsize) - - def color(self, weight): - weight = min(max(weight, 0.0), 1.0) - - hmin, smin, lmin = self.mincolor - hmax, smax, lmax = self.maxcolor - - h = hmin + weight*(hmax - hmin) - s = smin + weight*(smax - smin) - l = lmin + weight*(lmax - lmin) - - return self.hsl_to_rgb(h, s, l) - - def hsl_to_rgb(self, h, s, l): - """Convert a color from HSL color-model to RGB. - - See also: - - http://www.w3.org/TR/css3-color/#hsl-color - """ - - h = h % 1.0 - s = min(max(s, 0.0), 1.0) - l = min(max(l, 0.0), 1.0) - - if l <= 0.5: - m2 = l*(s + 1.0) - else: - m2 = l + s - l*s - m1 = l*2.0 - m2 - r = self._hue_to_rgb(m1, m2, h + 1.0/3.0) - g = self._hue_to_rgb(m1, m2, h) - b = self._hue_to_rgb(m1, m2, h - 1.0/3.0) - - # Apply gamma correction - r **= self.gamma - g **= self.gamma - b **= self.gamma - - return (r, g, b) - - def _hue_to_rgb(self, m1, m2, h): - if h < 0.0: - h += 1.0 - elif h > 1.0: - h -= 1.0 - if h*6 < 1.0: - return m1 + (m2 - m1)*h*6.0 - elif h*2 < 1.0: - return m2 - elif h*3 < 2.0: - return m1 + (m2 - m1)*(2.0/3.0 - h)*6.0 - else: - return m1 - - -TEMPERATURE_COLORMAP = Theme( - mincolor = (2.0/3.0, 0.80, 0.25), # dark blue - maxcolor = (0.0, 1.0, 0.5), # satured red - gamma = 1.0 -) - -PINK_COLORMAP = Theme( - mincolor = (0.0, 1.0, 0.90), # pink - maxcolor = (0.0, 1.0, 0.5), # satured red -) - -GRAY_COLORMAP = Theme( - mincolor = (0.0, 0.0, 0.85), # light gray - maxcolor = (0.0, 0.0, 0.0), # black -) - -BW_COLORMAP = Theme( - minfontsize = 8.0, - maxfontsize = 24.0, - mincolor = (0.0, 0.0, 0.0), # black - maxcolor = (0.0, 0.0, 0.0), # black - minpenwidth = 0.1, - maxpenwidth = 8.0, -) - - -class DotWriter: - """Writer for the DOT language. - - See also: - - "The DOT Language" specification - http://www.graphviz.org/doc/info/lang.html - """ - - def __init__(self, fp): - self.fp = fp - - def graph(self, profile, theme): - self.begin_graph() - - fontname = theme.graph_fontname() - - self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125) - self.attr('node', fontname=fontname, shape="box", style="filled,rounded", fontcolor="white", width=0, height=0) - self.attr('edge', fontname=fontname) - - for function in profile.functions.itervalues(): - labels = [] - for event in PROCESS, MODULE: - if event in function.events: - label = event.format(function[event]) - labels.append(label) - labels.append(function.name) - for event in TOTAL_TIME_RATIO, TIME_RATIO, CALLS: - if event in function.events: - label = event.format(function[event]) - labels.append(label) - - try: - weight = function[PRUNE_RATIO] - except UndefinedEvent: - weight = 0.0 - - label = '\n'.join(labels) - self.node(function.id, - label = label, - color = self.color(theme.node_bgcolor(weight)), - fontcolor = self.color(theme.node_fgcolor(weight)), - fontsize = "%.2f" % theme.node_fontsize(weight), - ) - - for call in function.calls.itervalues(): - callee = profile.functions[call.callee_id] - - labels = [] - for event in TOTAL_TIME_RATIO, CALLS: - if event in call.events: - label = event.format(call[event]) - labels.append(label) - - try: - weight = call[PRUNE_RATIO] - except UndefinedEvent: - try: - weight = callee[PRUNE_RATIO] - except UndefinedEvent: - weight = 0.0 - - label = '\n'.join(labels) - - self.edge(function.id, call.callee_id, - label = label, - color = self.color(theme.edge_color(weight)), - fontcolor = self.color(theme.edge_color(weight)), - fontsize = "%.2f" % theme.edge_fontsize(weight), - penwidth = "%.2f" % theme.edge_penwidth(weight), - labeldistance = "%.2f" % theme.edge_penwidth(weight), - arrowsize = "%.2f" % theme.edge_arrowsize(weight), - ) - - self.end_graph() - - def begin_graph(self): - self.write('digraph {\n') - - def end_graph(self): - self.write('}\n') - - def attr(self, what, **attrs): - self.write("\t") - self.write(what) - self.attr_list(attrs) - self.write(";\n") - - def node(self, node, **attrs): - self.write("\t") - self.id(node) - self.attr_list(attrs) - self.write(";\n") - - def edge(self, src, dst, **attrs): - self.write("\t") - self.id(src) - self.write(" -> ") - self.id(dst) - self.attr_list(attrs) - self.write(";\n") - - def attr_list(self, attrs): - if not attrs: - return - self.write(' [') - first = True - for name, value in attrs.iteritems(): - if first: - first = False - else: - self.write(", ") - self.id(name) - self.write('=') - self.id(value) - self.write(']') - - def id(self, id): - if isinstance(id, (int, float)): - s = str(id) - elif isinstance(id, str): - if id.isalnum(): - s = id - else: - s = self.escape(id) - else: - raise TypeError - self.write(s) - - def color(self, (r, g, b)): - - def float2int(f): - if f <= 0.0: - return 0 - if f >= 1.0: - return 255 - return int(255.0*f + 0.5) - - return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)]) - - def escape(self, s): - s = s.encode('utf-8') - s = s.replace('\\', r'\\') - s = s.replace('\n', r'\n') - s = s.replace('\t', r'\t') - s = s.replace('"', r'\"') - return '"' + s + '"' - - def write(self, s): - self.fp.write(s) - - -class Main: - """Main program.""" - - themes = { - "color": TEMPERATURE_COLORMAP, - "pink": PINK_COLORMAP, - "gray": GRAY_COLORMAP, - "bw": BW_COLORMAP, - } - - def main(self): - """Main program.""" - - parser = optparse.OptionParser( - usage="\n\t%prog [options] [file] ...", - version="%%prog %s" % __version__) - parser.add_option( - '-o', '--output', metavar='FILE', - type="string", dest="output", - help="output filename [stdout]") - parser.add_option( - '-n', '--node-thres', metavar='PERCENTAGE', - type="float", dest="node_thres", default=0.5, - help="eliminate nodes below this threshold [default: %default]") - parser.add_option( - '-e', '--edge-thres', metavar='PERCENTAGE', - type="float", dest="edge_thres", default=0.1, - help="eliminate edges below this threshold [default: %default]") - parser.add_option( - '-f', '--format', - type="choice", choices=('prof', 'oprofile', 'pstats', 'shark'), - dest="format", default="prof", - help="profile format: prof, oprofile, or pstats [default: %default]") - parser.add_option( - '-c', '--colormap', - type="choice", choices=('color', 'pink', 'gray', 'bw'), - dest="theme", default="color", - help="color map: color, pink, gray, or bw [default: %default]") - parser.add_option( - '-s', '--strip', - action="store_true", - dest="strip", default=False, - help="strip function parameters, template parameters, and const modifiers from demangled C++ function names") - parser.add_option( - '-w', '--wrap', - action="store_true", - dest="wrap", default=False, - help="wrap function names") - (self.options, self.args) = parser.parse_args(sys.argv[1:]) - - if len(self.args) > 1 and self.options.format != 'pstats': - parser.error('incorrect number of arguments') - - try: - self.theme = self.themes[self.options.theme] - except KeyError: - parser.error('invalid colormap \'%s\'' % self.options.theme) - - if self.options.format == 'prof': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = GprofParser(fp) - elif self.options.format == 'oprofile': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = OprofileParser(fp) - elif self.options.format == 'pstats': - if not self.args: - parser.error('at least a file must be specified for pstats input') - parser = PstatsParser(*self.args) - elif self.options.format == 'shark': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = SharkParser(fp) - else: - parser.error('invalid format \'%s\'' % self.options.format) - - self.profile = parser.parse() - - if self.options.output is None: - self.output = sys.stdout - else: - self.output = open(self.options.output, 'wt') - - self.write_graph() - - _parenthesis_re = re.compile(r'\([^()]*\)') - _angles_re = re.compile(r'<[^<>]*>') - _const_re = re.compile(r'\s+const$') - - def strip_function_name(self, name): - """Remove extraneous information from C++ demangled function names.""" - - # Strip function parameters from name by recursively removing paired parenthesis - while True: - name, n = self._parenthesis_re.subn('', name) - if not n: - break - - # Strip const qualifier - name = self._const_re.sub('', name) - - # Strip template parameters from name by recursively removing paired angles - while True: - name, n = self._angles_re.subn('', name) - if not n: - break - - return name - - def wrap_function_name(self, name): - """Split the function name on multiple lines.""" - - if len(name) > 32: - ratio = 2.0/3.0 - height = max(int(len(name)/(1.0 - ratio) + 0.5), 1) - width = max(len(name)/height, 32) - # TODO: break lines in symbols - name = textwrap.fill(name, width, break_long_words=False) - - # Take away spaces - name = name.replace(", ", ",") - name = name.replace("> >", ">>") - name = name.replace("> >", ">>") # catch consecutive - - return name - - def compress_function_name(self, name): - """Compress function name according to the user preferences.""" - - if self.options.strip: - name = self.strip_function_name(name) - - if self.options.wrap: - name = self.wrap_function_name(name) - - # TODO: merge functions with same resulting name - - return name - - def write_graph(self): - dot = DotWriter(self.output) - profile = self.profile - profile.prune(self.options.node_thres/100.0, self.options.edge_thres/100.0) - - for function in profile.functions.itervalues(): - function.name = self.compress_function_name(function.name) - - dot.graph(profile, self.theme) - - -if __name__ == '__main__': - Main().main() diff --git a/lib/tvdb_api/tests/runtests.py b/lib/tvdb_api/tests/runtests.py deleted file mode 100644 index 35932cd3..00000000 --- a/lib/tvdb_api/tests/runtests.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python2 -#encoding:utf-8 -#author:dbr/Ben -#project:tvdb_api -#repository:http://github.com/dbr/tvdb_api -#license:unlicense (http://unlicense.org/) - -import sys -import unittest - -import test_tvdb_api - -def main(): - suite = unittest.TestSuite([ - unittest.TestLoader().loadTestsFromModule(test_tvdb_api) - ]) - - runner = unittest.TextTestRunner(verbosity=2) - result = runner.run(suite) - if result.wasSuccessful(): - return 0 - else: - return 1 - -if __name__ == '__main__': - sys.exit( - int(main()) - ) diff --git a/lib/tvdb_api/tests/test_tvdb_api.py b/lib/tvdb_api/tests/test_tvdb_api.py deleted file mode 100644 index 826796b3..00000000 --- a/lib/tvdb_api/tests/test_tvdb_api.py +++ /dev/null @@ -1,577 +0,0 @@ -#!/usr/bin/env python2 -#encoding:utf-8 -#author:dbr/Ben -#project:tvdb_api -#repository:http://github.com/dbr/tvdb_api -#license:unlicense (http://unlicense.org/) - -"""Unittests for tvdb_api -""" - -import os,os.path -import sys -print sys.path -import datetime -import unittest - -# Force parent directory onto path -#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -sys.path.insert(1, os.path.abspath('../../tests')) - - -import sickbeard - - -from tvdb_api import Tvdb -import tvdb_ui -from tvdb_api import (tvdb_shownotfound, tvdb_seasonnotfound, -tvdb_episodenotfound, tvdb_attributenotfound) -from lib import xmltodict -import lib - -class test_tvdb_basic(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) - - def test_different_case(self): - """Checks the auto-correction of show names is working. - It should correct the weirdly capitalised 'sCruBs' to 'Scrubs' - """ - self.assertEquals(self.t['scrubs'][1][4]['episodename'], 'My Old Lady') - self.assertEquals(self.t['sCruBs']['seriesname'], 'Scrubs') - - def test_spaces(self): - """Checks shownames with spaces - """ - self.assertEquals(self.t['My Name Is Earl']['seriesname'], 'My Name Is Earl') - self.assertEquals(self.t['My Name Is Earl'][1][4]['episodename'], 'Faked His Own Death') - - def test_numeric(self): - """Checks numeric show names - """ - self.assertEquals(self.t['24'][2][20]['episodename'], 'Day 2: 3:00 A.M.-4:00 A.M.') - self.assertEquals(self.t['24']['seriesname'], '24') - - def test_show_iter(self): - """Iterating over a show returns each seasons - """ - self.assertEquals( - len( - [season for season in self.t['Life on Mars']] - ), - 2 - ) - - def test_season_iter(self): - """Iterating over a show returns episodes - """ - self.assertEquals( - len( - [episode for episode in self.t['Life on Mars'][1]] - ), - 8 - ) - - def test_get_episode_overview(self): - """Checks episode overview is retrieved correctly. - """ - self.assertEquals( - self.t['Battlestar Galactica (2003)'][1][6]['overview'].startswith( - 'When a new copy of Doral, a Cylon who had been previously'), - True - ) - - def test_get_parent(self): - """Check accessing series from episode instance - """ - show = self.t['Battlestar Galactica (2003)'] - season = show[1] - episode = show[1][1] - - self.assertEquals( - season.show, - show - ) - - self.assertEquals( - episode.season, - season - ) - - self.assertEquals( - episode.season.show, - show - ) - - def test_no_season(self): - show = self.t['Katekyo Hitman Reborn'] - print tvdb_api - print show[1][1] - - -class test_tvdb_errors(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) - - def test_seasonnotfound(self): - """Checks exception is thrown when season doesn't exist. - """ - self.assertRaises(tvdb_seasonnotfound, lambda:self.t['CNNNN'][10][1]) - - def test_shownotfound(self): - """Checks exception is thrown when episode doesn't exist. - """ - self.assertRaises(tvdb_shownotfound, lambda:self.t['the fake show thingy']) - - def test_episodenotfound(self): - """Checks exception is raised for non-existent episode - """ - self.assertRaises(tvdb_episodenotfound, lambda:self.t['Scrubs'][1][30]) - - def test_attributenamenotfound(self): - """Checks exception is thrown for if an attribute isn't found. - """ - self.assertRaises(tvdb_attributenotfound, lambda:self.t['CNNNN'][1][6]['afakeattributething']) - self.assertRaises(tvdb_attributenotfound, lambda:self.t['CNNNN']['afakeattributething']) - -class test_tvdb_search(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) - - def test_search_len(self): - """There should be only one result matching - """ - self.assertEquals(len(self.t['My Name Is Earl'].search('Faked His Own Death')), 1) - - def test_search_checkname(self): - """Checks you can get the episode name of a search result - """ - self.assertEquals(self.t['Scrubs'].search('my first')[0]['episodename'], 'My First Day') - self.assertEquals(self.t['My Name Is Earl'].search('Faked His Own Death')[0]['episodename'], 'Faked His Own Death') - - def test_search_multiresults(self): - """Checks search can return multiple results - """ - self.assertEquals(len(self.t['Scrubs'].search('my first')) >= 3, True) - - def test_search_no_params_error(self): - """Checks not supplying search info raises TypeError""" - self.assertRaises( - TypeError, - lambda: self.t['Scrubs'].search() - ) - - def test_search_season(self): - """Checks the searching of a single season""" - self.assertEquals( - len(self.t['Scrubs'][1].search("First")), - 3 - ) - - def test_search_show(self): - """Checks the searching of an entire show""" - self.assertEquals( - len(self.t['CNNNN'].search('CNNNN', key='episodename')), - 3 - ) - - def test_aired_on(self): - """Tests airedOn show method""" - sr = self.t['Scrubs'].airedOn(datetime.date(2001, 10, 2)) - self.assertEquals(len(sr), 1) - self.assertEquals(sr[0]['episodename'], u'My First Day') - -class test_tvdb_data(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) - - def test_episode_data(self): - """Check the firstaired value is retrieved - """ - self.assertEquals( - self.t['lost']['firstaired'], - '2004-09-22' - ) - -class test_tvdb_misc(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) - - def test_repr_show(self): - """Check repr() of Season - """ - self.assertEquals( - repr(self.t['CNNNN']), - "" - ) - def test_repr_season(self): - """Check repr() of Season - """ - self.assertEquals( - repr(self.t['CNNNN'][1]), - "" - ) - def test_repr_episode(self): - """Check repr() of Episode - """ - self.assertEquals( - repr(self.t['CNNNN'][1][1]), - "" - ) - def test_have_all_languages(self): - """Check valid_languages is up-to-date (compared to languages.xml) - """ - et = self.t._getetsrc( - "http://thetvdb.com/api/%s/languages.xml" % ( - self.t.config['apikey'] - ) - ) - languages = [x.find("abbreviation").text for x in et.findall("Language")] - - self.assertEquals( - sorted(languages), - sorted(self.t.config['valid_languages']) - ) - -class test_tvdb_languages(unittest.TestCase): - def test_episode_name_french(self): - """Check episode data is in French (language="fr") - """ - t = tvdb_api.Tvdb(cache = True, language = "fr") - self.assertEquals( - t['scrubs'][1][1]['episodename'], - "Mon premier jour" - ) - self.assertTrue( - t['scrubs']['overview'].startswith( - u"J.D. est un jeune m\xe9decin qui d\xe9bute" - ) - ) - - def test_episode_name_spanish(self): - """Check episode data is in Spanish (language="es") - """ - t = tvdb_api.Tvdb(cache = True, language = "es") - self.assertEquals( - t['scrubs'][1][1]['episodename'], - "Mi Primer Dia" - ) - self.assertTrue( - t['scrubs']['overview'].startswith( - u'Scrubs es una divertida comedia' - ) - ) - - def test_multilanguage_selection(self): - """Check selected language is used - """ - class SelectEnglishUI(tvdb_ui.BaseUI): - def selectSeries(self, allSeries): - return [x for x in allSeries if x['language'] == "en"][0] - - class SelectItalianUI(tvdb_ui.BaseUI): - def selectSeries(self, allSeries): - return [x for x in allSeries if x['language'] == "it"][0] - - t_en = tvdb_api.Tvdb( - cache=True, - custom_ui = SelectEnglishUI, - language = "en") - t_it = tvdb_api.Tvdb( - cache=True, - custom_ui = SelectItalianUI, - language = "it") - - self.assertEquals( - t_en['dexter'][1][2]['episodename'], "Crocodile" - ) - self.assertEquals( - t_it['dexter'][1][2]['episodename'], "Lacrime di coccodrillo" - ) - - -class test_tvdb_unicode(unittest.TestCase): - def test_search_in_chinese(self): - """Check searching for show with language=zh returns Chinese seriesname - """ - t = tvdb_api.Tvdb(cache = True, language = "zh") - show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'] - self.assertEquals( - type(show), - tvdb_api.Show - ) - - self.assertEquals( - show['seriesname'], - u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i' - ) - - def test_search_in_all_languages(self): - """Check search_all_languages returns Chinese show, with language=en - """ - t = tvdb_api.Tvdb(cache = True, search_all_languages = True, language="en") - show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'] - self.assertEquals( - type(show), - tvdb_api.Show - ) - - self.assertEquals( - show['seriesname'], - u'Virtues Of Harmony II' - ) - -class test_tvdb_banners(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = True) - - def test_have_banners(self): - """Check banners at least one banner is found - """ - self.assertEquals( - len(self.t['scrubs']['_banners']) > 0, - True - ) - - def test_banner_url(self): - """Checks banner URLs start with http:// - """ - for banner_type, banner_data in self.t['scrubs']['_banners'].items(): - for res, res_data in banner_data.items(): - for bid, banner_info in res_data.items(): - self.assertEquals( - banner_info['_bannerpath'].startswith("http://"), - True - ) - - def test_episode_image(self): - """Checks episode 'filename' image is fully qualified URL - """ - self.assertEquals( - self.t['scrubs'][1][1]['filename'].startswith("http://"), - True - ) - - def test_show_artwork(self): - """Checks various image URLs within season data are fully qualified - """ - for key in ['banner', 'fanart', 'poster']: - self.assertEquals( - self.t['scrubs'][key].startswith("http://"), - True - ) - -class test_tvdb_actors(unittest.TestCase): - t = None - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True) - - def test_actors_is_correct_datatype(self): - """Check show/_actors key exists and is correct type""" - self.assertTrue( - isinstance( - self.t['scrubs']['_actors'], - tvdb_api.Actors - ) - ) - - def test_actors_has_actor(self): - """Check show has at least one Actor - """ - self.assertTrue( - isinstance( - self.t['scrubs']['_actors'][0], - tvdb_api.Actor - ) - ) - - def test_actor_has_name(self): - """Check first actor has a name""" - self.assertEquals( - self.t['scrubs']['_actors'][0]['name'], - "Zach Braff" - ) - - def test_actor_image_corrected(self): - """Check image URL is fully qualified - """ - for actor in self.t['scrubs']['_actors']: - if actor['image'] is not None: - # Actor's image can be None, it displays as the placeholder - # image on thetvdb.com - self.assertTrue( - actor['image'].startswith("http://") - ) - -class test_tvdb_doctest(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False) - - def test_doctest(self): - """Check docstring examples works""" - import doctest - doctest.testmod(tvdb_api) - - -class test_tvdb_custom_caching(unittest.TestCase): - def test_true_false_string(self): - """Tests setting cache to True/False/string - - Basic tests, only checking for errors - """ - - tvdb_api.Tvdb(cache = True) - tvdb_api.Tvdb(cache = False) - tvdb_api.Tvdb(cache = "/tmp") - - def test_invalid_cache_option(self): - """Tests setting cache to invalid value - """ - - try: - tvdb_api.Tvdb(cache = 2.3) - except ValueError: - pass - else: - self.fail("Expected ValueError from setting cache to float") - - def test_custom_urlopener(self): - class UsedCustomOpener(Exception): - pass - - import urllib2 - class TestOpener(urllib2.BaseHandler): - def default_open(self, request): - print request.get_method() - raise UsedCustomOpener("Something") - - custom_opener = urllib2.build_opener(TestOpener()) - t = tvdb_api.Tvdb(cache = custom_opener) - try: - t['scrubs'] - except UsedCustomOpener: - pass - else: - self.fail("Did not use custom opener") - -class test_tvdb_by_id(unittest.TestCase): - t = None - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True) - - def test_actors_is_correct_datatype(self): - """Check show/_actors key exists and is correct type""" - self.assertEquals( - self.t[76156]['seriesname'], - 'Scrubs' - ) - - -class test_tvdb_zip(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, useZip = True) - - def test_get_series_from_zip(self): - """ - """ - self.assertEquals(self.t['scrubs'][1][4]['episodename'], 'My Old Lady') - self.assertEquals(self.t['sCruBs']['seriesname'], 'Scrubs') - - def test_spaces_from_zip(self): - """Checks shownames with spaces - """ - self.assertEquals(self.t['My Name Is Earl']['seriesname'], 'My Name Is Earl') - self.assertEquals(self.t['My Name Is Earl'][1][4]['episodename'], 'Faked His Own Death') - - -class test_tvdb_show_ordering(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t_dvd = None - t_air = None - - def setUp(self): - if self.t_dvd is None: - self.t_dvd = tvdb_api.Tvdb(cache = True, useZip = True, dvdorder=True) - - if self.t_air is None: - self.t_air = tvdb_api.Tvdb(cache = True, useZip = True) - - def test_ordering(self): - """Test Tvdb.search method - """ - self.assertEquals(u'The Train Job', self.t_air['Firefly'][1][1]['episodename']) - self.assertEquals(u'Serenity', self.t_dvd['Firefly'][1][1]['episodename']) - - self.assertEquals(u'The Cat & the Claw (Part 1)', self.t_air['Batman The Animated Series'][1][1]['episodename']) - self.assertEquals(u'On Leather Wings', self.t_dvd['Batman The Animated Series'][1][1]['episodename']) - -class test_tvdb_show_search(unittest.TestCase): - # Used to store the cached instance of Tvdb() - t = None - - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, useZip = True) - - def test_search(self): - """Test Tvdb.search method - """ - results = self.t.search("my name is earl") - all_ids = [x['seriesid'] for x in results] - self.assertTrue('75397' in all_ids) - - -class test_tvdb_alt_names(unittest.TestCase): - t = None - def setUp(self): - if self.t is None: - self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True) - - def test_1(self): - """Tests basic access of series name alias - """ - results = self.t.search("Don't Trust the B---- in Apartment 23") - series = results[0] - self.assertTrue( - 'Apartment 23' in series['aliasnames'] - ) - - -if __name__ == '__main__': - runner = unittest.TextTestRunner(verbosity = 2) - unittest.main(testRunner = runner) diff --git a/lib/tvdb_api/tvdb_api.py b/lib/tvdb_api/tvdb_api.py index d6396132..ad3ce83d 100644 --- a/lib/tvdb_api/tvdb_api.py +++ b/lib/tvdb_api/tvdb_api.py @@ -1,18 +1,17 @@ # !/usr/bin/env python2 -#encoding:utf-8 -#author:dbr/Ben -#project:tvdb_api -#repository:http://github.com/dbr/tvdb_api -#license:unlicense (http://unlicense.org/) +# encoding:utf-8 +# author:dbr/Ben +# project:tvdb_api +# repository:http://github.com/dbr/tvdb_api +# license:unlicense (http://unlicense.org/) from functools import wraps import traceback -__author__ = "dbr/Ben" -__version__ = "1.9" +__author__ = 'dbr/Ben' +__version__ = '1.9' import os -import re import time import getpass import StringIO @@ -20,7 +19,6 @@ import tempfile import warnings import logging import zipfile -import datetime as dt import requests import requests.exceptions import xmltodict @@ -39,12 +37,12 @@ from lib.dateutil.parser import parse from lib.cachecontrol import CacheControl, caches from tvdb_ui import BaseUI, ConsoleUI -from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound, +from tvdb_exceptions import (tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound) def log(): - return logging.getLogger("tvdb_api") + return logging.getLogger('tvdb_api') def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): @@ -76,7 +74,7 @@ def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): try: return f(*args, **kwargs) except ExceptionToCheck, e: - msg = "%s, Retrying in %d seconds..." % (str(e), mdelay) + msg = '%s, Retrying in %d seconds...' % (str(e), mdelay) if logger: logger.warning(msg) else: @@ -99,10 +97,10 @@ class ShowContainer(dict): self._stack = [] self._lastgc = time.time() - def __setitem__(self, key, value): + def __set_item__(self, key, value): self._stack.append(key) - #keep only the 100th latest results + # keep only the 100th latest results if time.time() - self._lastgc > 20: for o in self._stack[:-100]: del self[o] @@ -111,7 +109,7 @@ class ShowContainer(dict): self._lastgc = time.time() - super(ShowContainer, self).__setitem__(key, value) + super(ShowContainer, self).__set_item__(key, value) class Show(dict): @@ -123,10 +121,7 @@ class Show(dict): self.data = {} def __repr__(self): - return "" % ( - self.data.get(u'seriesname', 'instance'), - len(self) - ) + return '' % (self.data.get(u'seriesname', 'instance'), len(self)) def __getattr__(self, key): if key in self: @@ -151,16 +146,16 @@ class Show(dict): # Data wasn't found, raise appropriate error if isinstance(key, int) or key.isdigit(): # Episode number x was not found - raise tvdb_seasonnotfound("Could not find season %s" % (repr(key))) + raise tvdb_seasonnotfound('Could not find season %s' % (repr(key))) else: # If it's not numeric, it must be an attribute name, which # doesn't exist, so attribute error. - raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) + raise tvdb_attributenotfound('Cannot find attribute %s' % (repr(key))) def airedOn(self, date): ret = self.search(str(date), 'firstaired') - if len(ret) == 0: - raise tvdb_episodenotfound("Could not find any episodes that aired on %s" % date) + if 0 == len(ret): + raise tvdb_episodenotfound('Could not find any episodes that aired on %s' % date) return ret def search(self, term=None, key=None): @@ -181,43 +176,43 @@ class Show(dict): These examples assume t is an instance of Tvdb(): - >>> t = Tvdb() - >>> + >> t = Tvdb() + >> To search for all episodes of Scrubs with a bit of data containing "my first day": - >>> t['Scrubs'].search("my first day") + >> t['Scrubs'].search("my first day") [] - >>> + >> Search for "My Name Is Earl" episode named "Faked His Own Death": - >>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename') + >> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename') [] - >>> + >> To search Scrubs for all episodes with "mentor" in the episode name: - >>> t['scrubs'].search('mentor', key = 'episodename') + >> t['scrubs'].search('mentor', key = 'episodename') [, ] - >>> + >> # Using search results - >>> results = t['Scrubs'].search("my first") - >>> print results[0]['episodename'] + >> results = t['Scrubs'].search("my first") + >> print results[0]['episodename'] My First Day - >>> for x in results: print x['episodename'] + >> for x in results: print x['episodename'] My First Day My First Step My First Kill - >>> + >> """ results = [] for cur_season in self.values(): searchresult = cur_season.search(term=term, key=key) - if len(searchresult) != 0: + if 0 != len(searchresult): results.extend(searchresult) return results @@ -230,9 +225,7 @@ class Season(dict): self.show = show def __repr__(self): - return "" % ( - len(self.keys()) - ) + return '' % (len(self.keys())) def __getattr__(self, episode_number): if episode_number in self: @@ -241,7 +234,7 @@ class Season(dict): def __getitem__(self, episode_number): if episode_number not in self: - raise tvdb_episodenotfound("Could not find episode %s" % (repr(episode_number))) + raise tvdb_episodenotfound('Could not find episode %s' % (repr(episode_number))) else: return dict.__getitem__(self, episode_number) @@ -249,20 +242,18 @@ class Season(dict): """Search all episodes in season, returns a list of matching Episode instances. - >>> t = Tvdb() - >>> t['scrubs'][1].search('first day') + >> t = Tvdb() + >> t['scrubs'][1].search('first day') [] - >>> + >> See Show.search documentation for further information on search """ results = [] for ep in self.values(): searchresult = ep.search(term=term, key=key) - if searchresult is not None: - results.append( - searchresult - ) + if None is not searchresult: + results.append(searchresult) return results @@ -273,13 +264,12 @@ class Episode(dict): self.season = season def __repr__(self): - seasno = int(self.get(u'seasonnumber', 0)) - epno = int(self.get(u'episodenumber', 0)) + seasno, epno = int(self.get(u'seasonnumber', 0)), int(self.get(u'episodenumber', 0)) epname = self.get(u'episodename') - if epname is not None: - return "" % (seasno, epno, epname) + if None is not epname: + return '' % (seasno, epno, epname) else: - return "" % (seasno, epno) + return '' % (seasno, epno) def __getattr__(self, key): if key in self: @@ -290,37 +280,37 @@ class Episode(dict): try: return dict.__getitem__(self, key) except KeyError: - raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) + raise tvdb_attributenotfound('Cannot find attribute %s' % (repr(key))) def search(self, term=None, key=None): """Search episode data for term, if it matches, return the Episode (self). The key parameter can be used to limit the search to a specific element, for example, episodename. - + This primarily for use use by Show.search and Season.search. See Show.search for further information on search Simple example: - >>> e = Episode() - >>> e['episodename'] = "An Example" - >>> e.search("examp") + >> e = Episode() + >> e['episodename'] = "An Example" + >> e.search("examp") - >>> + >> Limiting by key: - >>> e.search("examp", key = "episodename") + >> e.search("examp", key = "episodename") - >>> + >> """ - if term == None: - raise TypeError("must supply string to search for (contents)") + if None is term: + raise TypeError('must supply string to search for (contents)') term = unicode(term).lower() for cur_key, cur_value in self.items(): cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower() - if key is not None and cur_key != key: + if None is not key and cur_key != key: # Do not search this key continue if cur_value.find(unicode(term).lower()) > -1: @@ -344,13 +334,13 @@ class Actor(dict): """ def __repr__(self): - return "" % (self.get("name")) + return '' % self.get('name') class Tvdb: """Create easy-to-use interface to name of season/episode name - >>> t = Tvdb() - >>> t['Scrubs'][1][24]['episodename'] + >> t = Tvdb() + >> t['Scrubs'][1][24]['episodename'] u'My Last Day' """ @@ -382,8 +372,8 @@ class Tvdb: debug (True/False) DEPRECATED: Replaced with proper use of logging module. To show debug messages: - >>> import logging - >>> logging.basicConfig(level = logging.DEBUG) + >> import logging + >> logging.basicConfig(level = logging.DEBUG) cache (True/False/str/unicode/urllib2 opener): Retrieved XML are persisted to to disc. If true, stores in @@ -397,15 +387,15 @@ class Tvdb: Retrieves the banners for a show. These are accessed via the _banners key of a Show(), for example: - >>> Tvdb(banners=True)['scrubs']['_banners'].keys() + >> Tvdb(banners=True)['scrubs']['_banners'].keys() ['fanart', 'poster', 'series', 'season'] actors (True/False): Retrieves a list of the actors for a show. These are accessed via the _actors key of a Show(), for example: - >>> t = Tvdb(actors=True) - >>> t['scrubs']['_actors'][0]['name'] + >> t = Tvdb(actors=True) + >> t['scrubs']['_actors'][0]['name'] u'Zach Braff' custom_ui (tvdb_ui.BaseUI subclass): @@ -415,14 +405,14 @@ class Tvdb: The language of the returned data. Is also the language search uses. Default is "en" (English). For full list, run.. - >>> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS + >> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS ['da', 'fi', 'nl', ...] search_all_languages (True/False): By default, Tvdb will only search in the language specified using the language option. When this is True, it will search for the show in and language - + apikey (str/unicode): Override the default thetvdb.com API key. By default it will use tvdb_api's own key (fine for small scripts), but you can use your @@ -447,10 +437,10 @@ class Tvdb: self.config = {} - if apikey is not None: + if None is not apikey: self.config['apikey'] = apikey else: - self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key + self.config['apikey'] = '0629B785CE550C8D' # tvdb_api's API key self.config['debug_enabled'] = debug # show debugging messages @@ -470,31 +460,30 @@ class Tvdb: if cache is True: self.config['cache_enabled'] = True - self.config['cache_location'] = self._getTempDir() + self.config['cache_location'] = self._get_temp_dir() elif cache is False: self.config['cache_enabled'] = False elif isinstance(cache, basestring): self.config['cache_enabled'] = True self.config['cache_location'] = cache else: - raise ValueError("Invalid value for Cache %r (type was %s)" % (cache, type(cache))) + raise ValueError('Invalid value for Cache %r (type was %s)' % (cache, type(cache))) self.config['banners_enabled'] = banners self.config['actors_enabled'] = actors if self.config['debug_enabled']: - warnings.warn("The debug argument to tvdb_api.__init__ will be removed in the next version. " - "To enable debug messages, use the following code before importing: " - "import logging; logging.basicConfig(level=logging.DEBUG)") + warnings.warn('The debug argument to tvdb_api.__init__ will be removed in the next version. ' + + 'To enable debug messages, use the following code before importing: ' + + 'import logging; logging.basicConfig(level=logging.DEBUG)') logging.basicConfig(level=logging.DEBUG) - # List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml # Hard-coded here as it is realtively static, and saves another HTTP request, as # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml self.config['valid_languages'] = [ - "da", "fi", "nl", "de", "it", "es", "fr", "pl", "hu", "el", "tr", - "ru", "he", "ja", "pt", "zh", "cs", "sl", "hr", "ko", "en", "sv", "no" + 'da', 'fi', 'nl', 'de', 'it', 'es', 'fr', 'pl', 'hu', 'el', 'tr', + 'ru', 'he', 'ja', 'pt', 'zh', 'cs', 'sl', 'hr', 'ko', 'en', 'sv', 'no' ] # thetvdb.com should be based around numeric language codes, @@ -506,58 +495,52 @@ class Tvdb: 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30} - if language is None: + if None is language: self.config['language'] = 'en' else: if language not in self.config['valid_languages']: - raise ValueError("Invalid language %s, options are: %s" % ( - language, self.config['valid_languages'] - )) + raise ValueError('Invalid language %s, options are: %s' % (language, self.config['valid_languages'])) else: self.config['language'] = language # The following url_ configs are based of the # http://thetvdb.com/wiki/index.php/Programmers_API - self.config['base_url'] = "http://thetvdb.com" + self.config['base_url'] = 'http://thetvdb.com' if self.config['search_all_languages']: - self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php" % self.config - self.config['params_getSeries'] = {"seriesname": "", "language": "all"} + self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config + self.config['params_get_series'] = {'seriesname': '', 'language': 'all'} else: - self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php" % self.config - self.config['params_getSeries'] = {"seriesname": "", "language": self.config['language']} + self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config + self.config['params_get_series'] = {'seriesname': '', 'language': self.config['language']} - self.config['url_epInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml" % self.config - self.config['url_epInfo_zip'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip" % self.config + self.config['url_epInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml' % self.config + self.config['url_epInfo_zip'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip' % self.config - self.config['url_seriesInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml" % self.config - self.config['url_actorsInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config + self.config['url_seriesInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml' % self.config + self.config['url_actorsInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/actors.xml' % self.config - self.config['url_seriesBanner'] = u"%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config - self.config['url_artworkPrefix'] = u"%(base_url)s/banners/%%s" % self.config + self.config['url_seriesBanner'] = u'%(base_url)s/api/%(apikey)s/series/%%s/banners.xml' % self.config + self.config['url_artworkPrefix'] = u'%(base_url)s/banners/%%s' % self.config - self.config['url_updates_all'] = u"%(base_url)s/api/%(apikey)s/updates_all.zip" % self.config - self.config['url_updates_month'] = u"%(base_url)s/api/%(apikey)s/updates_month.zip" % self.config - self.config['url_updates_week'] = u"%(base_url)s/api/%(apikey)s/updates_week.zip" % self.config - self.config['url_updates_day'] = u"%(base_url)s/api/%(apikey)s/updates_day.zip" % self.config - - def _getTempDir(self): + @staticmethod + def _get_temp_dir(): """Returns the [system temp dir]/tvdb_api-u501 (or tvdb_api-myuser) """ if hasattr(os, 'getuid'): - uid = "u%d" % (os.getuid()) + uid = 'u%d' % (os.getuid()) else: # For Windows try: uid = getpass.getuser() except ImportError: - return os.path.join(tempfile.gettempdir(), "tvdb_api") + return os.path.join(tempfile.gettempdir(), 'tvdb_api') - return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid)) + return os.path.join(tempfile.gettempdir(), 'tvdb_api-%s' % uid) @retry(tvdb_error) - def _loadUrl(self, url, params=None, language=None): + def _load_url(self, url, params=None, language=None): log().debug('Retrieving URL %s' % url) session = requests.session() @@ -587,21 +570,11 @@ class Tvdb: # clean up value and do type changes if value: - try: - if key == 'firstaired' and value in '0000-00-00': - new_value = str(dt.date.fromordinal(1)) - new_value = re.sub('([-]0{2})+', '', new_value) - fix_date = parse(new_value, fuzzy=True).date() - value = fix_date.strftime('%Y-%m-%d') - elif key == 'firstaired': - value = parse(value, fuzzy=True).date() - value = value.strftime('%Y-%m-%d') - - #if key == 'airs_time': - # value = parse(value).time() - # value = value.strftime('%I:%M %p') - except: - pass + if 'firstaired' == key: + try: + value = parse(value, fuzzy=True).strftime('%Y-%m-%d') + except: + value = None return key, value @@ -626,14 +599,14 @@ class Tvdb: """Loads a URL using caching, returns an ElementTree of the source """ try: - src = self._loadUrl(url, params=params, language=language).values()[0] + src = self._load_url(url, params=params, language=language).values()[0] return src except: return [] - def _setItem(self, sid, seas, ep, attrib, value): + def _set_item(self, sid, seas, ep, attrib, value): """Creates a new episode, creating Show(), Season() and - Episode()s as required. Called by _getShowData to populate show + Episode()s as required. Called by _get_show_data to populate show Since the nice-to-use tvdb[1][24]['name] interface makes it impossible to do tvdb[1][24]['name] = "name" @@ -654,59 +627,57 @@ class Tvdb: self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas]) self.shows[sid][seas][ep][attrib] = value - def _setShowData(self, sid, key, value): + def _set_show_data(self, sid, key, value): """Sets self.shows[sid] to a new Show instance, or sets the data """ if sid not in self.shows: self.shows[sid] = Show() self.shows[sid].data[key] = value - def _cleanData(self, data): + def _clean_data(self, data): """Cleans up strings returned by TheTVDB.com Issues corrected: - Replaces & with & - Trailing whitespace """ - data = data.replace(u"&", u"&") - data = data.strip() - return data + return data if data is None else data.strip().replace(u'&', u'&') def search(self, series): """This searches TheTVDB.com for the series name and returns the result list """ - series = series.encode("utf-8") - log().debug("Searching for show %s" % series) - self.config['params_getSeries']['seriesname'] = series + series = series.encode('utf-8') + log().debug('Searching for show %s' % series) + self.config['params_get_series']['seriesname'] = series try: - seriesFound = self._getetsrc(self.config['url_getSeries'], self.config['params_getSeries']) - if seriesFound: - return seriesFound.values()[0] + series_found = self._getetsrc(self.config['url_get_series'], self.config['params_get_series']) + if series_found: + return series_found.values()[0] except: pass return [] - def _getSeries(self, series): + def _get_series(self, series): """This searches TheTVDB.com for the series name, If a custom_ui UI is configured, it uses this to select the correct series. If not, and interactive == True, ConsoleUI is used, if not BaseUI is used to select the first result. """ - allSeries = self.search(series) - if not isinstance(allSeries, list): - allSeries = [allSeries] + all_series = self.search(series) + if not isinstance(all_series, list): + all_series = [all_series] - if len(allSeries) == 0: + if 0 == len(all_series): log().debug('Series result returned zero') - raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)") + raise tvdb_shownotfound('Show-name search returned zero results (cannot find show on TVDB)') - if self.config['custom_ui'] is not None: - log().debug("Using custom UI %s" % (repr(self.config['custom_ui']))) - CustomUI = self.config['custom_ui'] - ui = CustomUI(config=self.config) + if None is not self.config['custom_ui']: + log().debug('Using custom UI %s' % (repr(self.config['custom_ui']))) + custom_ui = self.config['custom_ui'] + ui = custom_ui(config=self.config) else: if not self.config['interactive']: log().debug('Auto-selecting first search result using BaseUI') @@ -715,156 +686,151 @@ class Tvdb: log().debug('Interactively selecting show using ConsoleUI') ui = ConsoleUI(config=self.config) - return ui.selectSeries(allSeries) + return ui.selectSeries(all_series) - def _parseBanners(self, sid): + def _parse_banners(self, sid): """Parses banners XML, from http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml Banners are retrieved using t['show name]['_banners'], for example: - >>> t = Tvdb(banners = True) - >>> t['scrubs']['_banners'].keys() + >> t = Tvdb(banners = True) + >> t['scrubs']['_banners'].keys() ['fanart', 'poster', 'series', 'season'] - >>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath'] + >> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath'] u'http://thetvdb.com/banners/posters/76156-2.jpg' - >>> + >> Any key starting with an underscore has been processed (not the raw data from the XML) This interface will be improved in future versions. """ - log().debug('Getting season banners for %s' % (sid)) - bannersEt = self._getetsrc(self.config['url_seriesBanner'] % (sid)) + log().debug('Getting season banners for %s' % sid) + banners_et = self._getetsrc(self.config['url_seriesBanner'] % sid) banners = {} try: - for cur_banner in bannersEt['banner']: + for cur_banner in banners_et['banner']: bid = cur_banner['id'] btype = cur_banner['bannertype'] btype2 = cur_banner['bannertype2'] - if btype is None or btype2 is None: + if None is btype or None is btype2: continue - if not btype in banners: + if btype not in banners: banners[btype] = {} - if not btype2 in banners[btype]: + if btype2 not in banners[btype]: banners[btype][btype2] = {} - if not bid in banners[btype][btype2]: + if bid not in banners[btype][btype2]: banners[btype][btype2][bid] = {} for k, v in cur_banner.items(): - if k is None or v is None: + if None is k or None is v: continue k, v = k.lower(), v.lower() banners[btype][btype2][bid][k] = v for k, v in banners[btype][btype2][bid].items(): - if k.endswith("path"): - new_key = "_%s" % (k) - log().debug("Transforming %s to %s" % (k, new_key)) - new_url = self.config['url_artworkPrefix'] % (v) + if k.endswith('path'): + new_key = '_%s' % k + log().debug('Transforming %s to %s' % (k, new_key)) + new_url = self.config['url_artworkPrefix'] % v banners[btype][btype2][bid][new_key] = new_url except: pass - self._setShowData(sid, "_banners", banners) + self._set_show_data(sid, '_banners', banners) - def _parseActors(self, sid): + def _parse_actors(self, sid): """Parsers actors XML, from http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml Actors are retrieved using t['show name]['_actors'], for example: - >>> t = Tvdb(actors = True) - >>> actors = t['scrubs']['_actors'] - >>> type(actors) + >> t = Tvdb(actors = True) + >> actors = t['scrubs']['_actors'] + >> type(actors) - >>> type(actors[0]) + >> type(actors[0]) - >>> actors[0] + >> actors[0] - >>> sorted(actors[0].keys()) + >> sorted(actors[0].keys()) ['id', 'image', 'name', 'role', 'sortorder'] - >>> actors[0]['name'] + >> actors[0]['name'] u'Zach Braff' - >>> actors[0]['image'] + >> actors[0]['image'] u'http://thetvdb.com/banners/actors/43640.jpg' Any key starting with an underscore has been processed (not the raw data from the XML) """ - log().debug("Getting actors for %s" % (sid)) - actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid)) + log().debug('Getting actors for %s' % sid) + actors_et = self._getetsrc(self.config['url_actorsInfo'] % sid) cur_actors = Actors() try: - for curActorItem in actorsEt["actor"]: - curActor = Actor() + for curActorItem in actors_et['actor']: + cur_actor = Actor() for k, v in curActorItem.items(): k = k.lower() - if v is not None: - if k == "image": - v = self.config['url_artworkPrefix'] % (v) + if None is not v: + if 'image' == k: + v = self.config['url_artworkPrefix'] % v else: - v = self._cleanData(v) - curActor[k] = v - cur_actors.append(curActor) + v = self._clean_data(v) + cur_actor[k] = v + cur_actors.append(cur_actor) except: pass - self._setShowData(sid, '_actors', cur_actors) + self._set_show_data(sid, '_actors', cur_actors) - def _getShowData(self, sid, language, getEpInfo=False): + def _get_show_data(self, sid, language, get_ep_info=False): """Takes a series ID, gets the epInfo URL and parses the TVDB XML file into the shows dict in layout: shows[series_id][season_number][episode_number] """ - if self.config['language'] is None: + if None is self.config['language']: log().debug('Config language is none, using show language') - if language is None: - raise tvdb_error("config['language'] was None, this should not happen") - getShowInLanguage = language + if None is language: + raise tvdb_error('config[\'language\'] was None, this should not happen') + get_show_in_language = language else: - log().debug( - 'Configured language %s override show language of %s' % ( - self.config['language'], - language - ) - ) - getShowInLanguage = self.config['language'] + log().debug('Configured language %s override show language of %s' % (self.config['language'], language)) + get_show_in_language = self.config['language'] # Parse show information - log().debug('Getting all series data for %s' % (sid)) + log().debug('Getting all series data for %s' % sid) url = self.config['url_epInfo%s' % ('', '_zip')[self.config['useZip']]] % (sid, language) - show_data = self._getetsrc(url, language=getShowInLanguage) + show_data = self._getetsrc(url, language=get_show_in_language) # check and make sure we have data to process and that it contains a series name if not len(show_data) or (isinstance(show_data, dict) and 'seriesname' not in show_data['series']): return False for k, v in show_data['series'].items(): - if v is not None: + if None is not v: if k in ['banner', 'fanart', 'poster']: - v = self.config['url_artworkPrefix'] % (v) + v = self.config['url_artworkPrefix'] % v else: - v = self._cleanData(v) + v = self._clean_data(v) - self._setShowData(sid, k, v) + self._set_show_data(sid, k, v) - if getEpInfo: + if get_ep_info: # Parse banners if self.config['banners_enabled']: - self._parseBanners(sid) + self._parse_banners(sid) # Parse actors if self.config['actors_enabled']: - self._parseActors(sid) + self._parse_actors(sid) # Parse episode data - log().debug('Getting all episodes of %s' % (sid)) + log().debug('Getting all episodes of %s' % sid) if 'episode' not in show_data: return False @@ -876,38 +842,38 @@ class Tvdb: for cur_ep in episodes: if self.config['dvdorder']: log().debug('Using DVD ordering.') - use_dvd = cur_ep['dvd_season'] != None and cur_ep['dvd_episodenumber'] != None + use_dvd = None is not cur_ep['dvd_season'] and None is not cur_ep['dvd_episodenumber'] else: use_dvd = False if use_dvd: - seasnum, epno = cur_ep['dvd_season'], cur_ep['dvd_episodenumber'] + elem_seasnum, elem_epno = cur_ep['dvd_season'], cur_ep['dvd_episodenumber'] else: - seasnum, epno = cur_ep['seasonnumber'], cur_ep['episodenumber'] + elem_seasnum, elem_epno = cur_ep['seasonnumber'], cur_ep['episodenumber'] - if seasnum is None or epno is None: - log().warning("An episode has incomplete season/episode number (season: %r, episode: %r)" % ( - seasnum, epno)) + if None is elem_seasnum or None is elem_epno: + log().warning('An episode has incomplete season/episode number (season: %r, episode: %r)' % ( + elem_seasnum, elem_epno)) continue # Skip to next episode # float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data - seas_no = int(float(seasnum)) - ep_no = int(float(epno)) + seas_no = int(float(elem_seasnum)) + ep_no = int(float(elem_epno)) for k, v in cur_ep.items(): k = k.lower() - if v is not None: - if k == 'filename': - v = self.config['url_artworkPrefix'] % (v) + if None is not v: + if 'filename' == k: + v = self.config['url_artworkPrefix'] % v else: - v = self._cleanData(v) + v = self._clean_data(v) - self._setItem(sid, seas_no, ep_no, k, v) + self._set_item(sid, seas_no, ep_no, k, v) return True - def _nameToSid(self, name): + def _name_to_sid(self, name): """Takes show name, returns the correct series ID (if the show has already been grabbed), or grabs all episodes and returns the correct SID. @@ -916,12 +882,12 @@ class Tvdb: log().debug('Correcting %s to %s' % (name, self.corrections[name])) return self.corrections[name] else: - log().debug('Getting show %s' % (name)) - selected_series = self._getSeries(name) + log().debug('Getting show %s' % name) + selected_series = self._get_series(name) if isinstance(selected_series, dict): selected_series = [selected_series] sids = list(int(x['id']) for x in selected_series if - self._getShowData(int(x['id']), self.config['language'])) + self._get_show_data(int(x['id']), self.config['language'])) self.corrections.update(dict((x['seriesname'], int(x['id'])) for x in selected_series)) return sids @@ -932,19 +898,16 @@ class Tvdb: if isinstance(key, (int, long)): # Item is integer, treat as show id if key not in self.shows: - self._getShowData(key, self.config['language'], True) + self._get_show_data(key, self.config['language'], True) return None if key not in self.shows else self.shows[key] key = str(key).lower() self.config['searchterm'] = key - selected_series = self._getSeries(key) + selected_series = self._get_series(key) if isinstance(selected_series, dict): selected_series = [selected_series] - [[self._setShowData(show['id'], k, v) for k, v in show.items()] for show in selected_series] + [[self._set_show_data(show['id'], k, v) for k, v in show.items()] for show in selected_series] return selected_series - #test = self._getSeries(key) - #sids = self._nameToSid(key) - #return list(self.shows[sid] for sid in sids) def __repr__(self): return str(self.shows) @@ -963,5 +926,5 @@ def main(): print tvdb_instance['Lost'][1][4]['episodename'] -if __name__ == '__main__': +if '__main__' == __name__: main()