Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ exclude_lines =
pragma: no cover
raise NotImplementedError
raise Unsupported
raise Exception
except ImportError
except BadRequest
def __repr__
def __bool__
if __name__ == .__main__.:
def __iter__
def __hash__
def __len__
if __name__ == .__main__.:
4 changes: 3 additions & 1 deletion plexapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@
logformat = CONFIG.get('log.format', '%(asctime)s %(module)12s:%(lineno)-4s %(levelname)-9s %(message)s')
loglevel = CONFIG.get('log.level', 'INFO').upper()
loghandler = logging.NullHandler()
if logfile:

if logfile: # pragma: no cover
logbackups = CONFIG.get('log.backup_count', 3, int)
logbytes = CONFIG.get('log.rotate_bytes', 512000, int)
loghandler = RotatingFileHandler(os.path.expanduser(logfile), 'a', logbytes, logbackups)

loghandler.setFormatter(logging.Formatter(logformat))
log.addHandler(loghandler)
log.setLevel(loglevel)
Expand Down
8 changes: 4 additions & 4 deletions plexapi/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def run(self):
url = self._server.url(self.key).replace('http', 'ws')
log.info('Starting AlertListener: %s', url)
self._ws = websocket.WebSocketApp(url, on_message=self._onMessage,
on_error=self._onError)
on_error=self._onError)
self._ws.run_forever()

def stop(self):
Expand All @@ -47,12 +47,12 @@ def _onMessage(self, ws, message):
""" Called when websocket message is recieved. """
try:
data = json.loads(message)['NotificationContainer']
log.debug('Alert: %s', data)
log.debug('Alert: %s %s %s', *data)
if self._callback:
self._callback(data)
except Exception as err:
except Exception as err: # pragma: no cover
log.error('AlertListener Msg Error: %s', err)

def _onError(self, ws, err):
def _onError(self, ws, err): # pragma: no cover
""" Called when websocket error is recieved. """
log.error('AlertListener Error: %s' % err)
2 changes: 1 addition & 1 deletion plexapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def playMedia(self, media, offset=0, **params):
if not self.product != 'OpenPHT':
try:
self.sendCommand('timeline/subscribe', port=server_url[1].strip('/'), protocol='http')
except:
except: # noqa: E722
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you say except Exception: I believe pyflakes wont yell at you.

# some clients dont need or like this and raises http 400.
# We want to include the exception in the log,
# but it might still work so we swallow it.
Expand Down
2 changes: 1 addition & 1 deletion plexapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def get(self, key, default=None, cast=None):
section, name = key.lower().split('.')
value = self.data.get(section, {}).get(name, default)
return cast(value) if cast else value
except:
except: # noqa: E722
return default

def _asDict(self):
Expand Down
2 changes: 1 addition & 1 deletion plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def _loadData(self, data):
self.type = cast(int, data.attrib.get('streamType'))

@staticmethod
def parse(server, data, initpath):
def parse(server, data, initpath): # pragma: no cover seems to be dead code.
""" Factory method returns a new MediaPartStream from xml data. """
STREAMCLS = {1: VideoStream, 2: AudioStream, 3: SubtitleStream}
stype = cast(int, data.attrib.get('streamType'))
Expand Down
8 changes: 4 additions & 4 deletions plexapi/myplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ def query(self, url, method=None, headers=None, timeout=None, **kwargs):
log.debug('%s %s %s', method.__name__.upper(), url, kwargs.get('json', ''))
headers = self._headers(**headers or {})
response = method(url, headers=headers, timeout=timeout, **kwargs)
if response.status_code not in (200, 201, 204):
if response.status_code not in (200, 201, 204): # pragma: no cover
codename = codes.get(response.status_code)[0]
errtext = response.text.replace('\n', ' ')
log.warn('BadRequest (%s) %s %s; %s' % (response.status_code, codename, response.url, errtext))
log.warning('BadRequest (%s) %s %s; %s' % (response.status_code, codename, response.url, errtext))
raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext))
data = response.text.encode('utf8')
return ElementTree.fromstring(data) if data.strip() else None
Expand Down Expand Up @@ -428,8 +428,8 @@ def _loadData(self, data):
self.recommendationsPlaylistId = data.attrib.get('recommendationsPlaylistId')
self.restricted = data.attrib.get('restricted')
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
self.username = data.attrib.get('username')
self.title = data.attrib.get('title', '')
self.username = data.attrib.get('username', '')
self.servers = self.findItems(data, MyPlexServerShare)

def get_token(self, machineIdentifier):
Expand Down
11 changes: 2 additions & 9 deletions plexapi/photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def albums(self, **kwargs):
def album(self, title):
""" Returns the :class:`~plexapi.photo.Photoalbum` that matches the specified title. """
for album in self.albums():
if album.attrib.get('title').lower() == title.lower():
if album.title.lower() == title.lower():
return album
raise NotFound('Unable to find album: %s' % title)

Expand All @@ -66,17 +66,10 @@ def photos(self, **kwargs):
def photo(self, title):
""" Returns the :class:`~plexapi.photo.Photo` that matches the specified title. """
for photo in self.photos():
if photo.attrib.get('title').lower() == title.lower():
if photo.title.lower() == title.lower():
return photo
raise NotFound('Unable to find photo: %s' % title)

def reload(self):
""" Reload the data for this object from self.key. """
self._initpath = self.key
data = self._server.query(self.key)
self._loadData(data)
return self


@utils.registerPlexObject
class Photo(PlexPartialObject):
Expand Down
10 changes: 5 additions & 5 deletions plexapi/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ def _loadData(self, data):
self.updatedAt = toDatetime(data.attrib.get('updatedAt'))
self._items = None # cache for self.items

def __len__(self):
def __len__(self): # pragma: no cover
return len(self.items())

def __contains__(self, other):
def __contains__(self, other): # pragma: no cover
return any(i.key == other.key for i in self.items())

def __getitem__(self, key):
def __getitem__(self, key): # pragma: no cover
return self.items()[key]

def items(self):
Expand All @@ -57,7 +57,7 @@ def addItems(self, items):
items = [items]
ratingKeys = []
for item in items:
if item.listType != self.playlistType:
if item.listType != self.playlistType: # pragma: no cover
raise BadRequest('Can not mix media types when building a playlist: %s and %s' %
(self.playlistType, item.listType))
ratingKeys.append(str(item.ratingKey))
Expand Down Expand Up @@ -108,7 +108,7 @@ def create(cls, server, title, items):
items = [items]
ratingKeys = []
for item in items:
if item.listType != items[0].listType:
if item.listType != items[0].listType: # pragma: no cover
raise BadRequest('Can not mix media types when building a playlist')
ratingKeys.append(str(item.ratingKey))
ratingKeys = ','.join(ratingKeys)
Expand Down
23 changes: 8 additions & 15 deletions plexapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,6 @@ def cast(func, value):
return value


def getattributeOrNone(obj, self, attr):
""" Returns result from __getattribute__ or None if not found. """
try:
return super(obj, self).__getattribute__(attr)
except AttributeError:
return None


def joinArgs(args):
""" Returns a query string (uses for HTTP URLs) where only the value is URL encoded.
Example return value: '?genre=action&type=1337'.
Expand Down Expand Up @@ -129,7 +121,7 @@ def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover
if attrstr:
return rget(value, attrstr, default, delim)
return value
except:
except: # noqa: E722
return default


Expand Down Expand Up @@ -198,7 +190,8 @@ def toList(value, itemcast=None, delim=','):
return [itemcast(item) for item in value.split(delim) if item != '']


def downloadSessionImages(server, filename=None, height=150, width=150, opacity=100, saturation=100):
def downloadSessionImages(server, filename=None, height=150, width=150,
opacity=100, saturation=100): # pragma: no cover
""" Helper to download a bif image or thumb.url from plex.server.sessions.

Parameters:
Expand Down Expand Up @@ -243,7 +236,7 @@ def download(url, filename=None, savepath=None, session=None, chunksize=4024,
mocked (bool): Helper to do evertything except write the file.
unpack (bool): Unpack the zip file.
showstatus(bool): Display a progressbar.

Example:
>>> download(a_episode.getStreamURL(), a_episode.location)
/path/to/file
Expand Down Expand Up @@ -278,7 +271,7 @@ def download(url, filename=None, savepath=None, session=None, chunksize=4024,

# save the file to disk
log.info('Downloading: %s', fullpath)
if showstatus:
if showstatus: # pragma: no cover
total = int(response.headers.get('content-length', 0))
bar = tqdm(unit='B', unit_scale=True, total=total, desc=filename)

Expand All @@ -288,7 +281,7 @@ def download(url, filename=None, savepath=None, session=None, chunksize=4024,
if showstatus:
bar.update(len(chunk))

if showstatus:
if showstatus: # pragma: no cover
bar.close()
# check we want to unzip the contents
if fullpath.endswith('zip') and unpack:
Expand All @@ -314,7 +307,7 @@ def tag_helper(tag, items, locked=True, remove=False):
return data


def getMyPlexAccount(opts=None):
def getMyPlexAccount(opts=None): # pragma: no cover
""" Helper function tries to get a MyPlex Account instance by checking
the the following locations for a username and password. This is
useful to create user-friendly command line tools.
Expand All @@ -341,7 +334,7 @@ def getMyPlexAccount(opts=None):
return MyPlexAccount(username, password)


def choose(msg, items, attr):
def choose(msg, items, attr): # pragma: no cover
""" Command line helper to display a list of choices, asking the
user to choose one of the options.
"""
Expand Down
39 changes: 35 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,24 @@
# 3. A Photos section containing the photoalbums:
# Cats (with cute cat photos inside)
# 4. A TV Shows section containing at least two seasons of The 100.
import plexapi, pytest, requests
from datetime import datetime
from functools import partial

import pytest
import requests

try:
from unittest.mock import patch, MagicMock
except ImportError:
from mock import patch, MagicMock


import plexapi
from plexapi import compat
from plexapi.client import PlexClient
from datetime import datetime

from plexapi.server import PlexServer
from functools import partial


SERVER_BASEURL = plexapi.CONFIG.get('auth.server_baseurl')
SERVER_TOKEN = plexapi.CONFIG.get('auth.server_token')
Expand Down Expand Up @@ -150,10 +162,29 @@ def monkeydownload(request, monkeypatch):
monkeypatch.undo()


def callable_http_patch():
return patch('plexapi.server.requests.sessions.Session.send',
return_value=MagicMock(status_code=200,
text='<xml><child></child></xml>'))


@pytest.fixture()
def empty_response(mocker):
response = mocker.MagicMock(status_code=200, text='<xml><child></child></xml>')
return response


@pytest.fixture()
def patched_http_call(mocker):
return mocker.patch('plexapi.server.requests.sessions.Session.send',
return_value=MagicMock(status_code=200,
text='<xml><child></child></xml>')
)


# ---------------------------------
# Utility Functions
# ---------------------------------

def is_datetime(value):
return value > MIN_DATETIME

Expand Down
37 changes: 23 additions & 14 deletions tests/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,8 @@ def test_library_section_get_movie(plex):
assert plex.library.section('Movies').get('Sita Sings the Blues')


def test_library_section_delete(monkeypatch, movies):
monkeypatch.delattr("requests.sessions.Session.request")
try:
movies.delete()
except AttributeError:
# will always raise because there is no request
pass
def test_library_section_delete(movies, patched_http_call):
movies.delete()


def test_library_fetchItem(plex, movie):
Expand All @@ -69,11 +64,6 @@ def test_library_recentlyAdded(plex):
assert len(list(plex.library.recentlyAdded()))


def test_library_search(plex):
item = plex.library.search('Elephants Dream')[0]
assert item.title == 'Elephants Dream'


def test_library_add_edit_delete(plex):
# Dont add a location to prevent scanning scanning
section_name = 'plexapi_test_section'
Expand Down Expand Up @@ -115,14 +105,33 @@ def test_library_Library_deleteMediaPreviews(plex):
plex.library.deleteMediaPreviews()


def _test_library_MovieSection_refresh(movies):
movies.refresh()
def test_library_Library_all(plex):
assert len(plex.library.all(title__iexact='The 100'))


def test_library_Library_search(plex):
item = plex.library.search('Elephants Dream')[0]
assert item.title == 'Elephants Dream'
assert len(plex.library.search(libtype='episode'))


def test_library_MovieSection_update(movies):
movies.update()


def test_library_ShowSection_all(tvshows):
assert len(tvshows.all(title__iexact='The 100'))


def test_library_MovieSection_refresh(movies, patched_http_call):
movies.refresh()


def test_library_MovieSection_search_genre(movie, movies):
# assert len(movie.genres[0].items()) # TODO
assert len(movies.search(genre=movie.genres[0])) > 1


def test_library_MovieSection_cancelUpdate(movies):
movies.cancelUpdate()

Expand Down
Loading