Skip to content

Commit

Permalink
docs: dosctrings and some linting for containers.py
Browse files Browse the repository at this point in the history
  • Loading branch information
Noiredd committed May 10, 2020
1 parent 7ede8e0 commit a4d948e
Showing 1 changed file with 124 additions and 18 deletions.
142 changes: 124 additions & 18 deletions filmatyk/containers.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,58 @@
from datetime import date

# This is a globally used dict that binds Item classes to their names.
# It should remain empty, as the classes register themselves here.
classByString = {}

class Blueprint(object):
"""Blueprint is an abstraction of a property that an Item might have.
Each property is a named piece of information that can be:
* acquired,
* stored,
* and presented
in a specific way, and is bound to the Item class.
Blueprint defines acquisition (i.e. how to extract it from Filmweb HTML) and
presentation (how to render it into presentable text) for that piece of
information.
Attributes:
* display_name: str, presentable name of the property (used to name column
headers in the Presenter)
* column_width: int, default width of a Presenter column,
* parsing_rule: dict, parsing rules for acquiring that property, for details
see filmweb.py and read ../readme/HOWITWORKS.md,
* display_rule: callable or None, (optional) function to convert raw property
into a string representation,
* store: bool, should that property be included when serializing a containing
instance.
Static methods define some basic, commonly used presentation functions for
known types of properties.
"""
@staticmethod
def _default(x): return str(x)

@staticmethod
def _list(x): return ', '.join(x)

@staticmethod
def _duration(x):
h = x // 60
m = x % 60
return '{0!s}h {1!s}m'.format(h, m)

@staticmethod
def _fwRating(x):
return str(round(x, 1))

@staticmethod
def _rating(x):
if x == 0:
return '-'
else:
return str(x) + ' ' + ''.join('★' for i in range(x))

@staticmethod
def _favourite(x):
return '♥' if x == 1 else ' '
Expand All @@ -31,16 +63,24 @@ def __init__(self, name:str, colwidth:int, parsing:dict={}, display=None, store=
self.parsing_rule = parsing if parsing else None
self.display_rule = display if display else self._default
self.store = store

def getParsing(self):
return self.parsing_rule

def getDisplay(self):
return self.display_rule

def getHeading(self):
return self.display_name

def getColWidth(self):
return self.column_width

class UserData(object):
"""Encapsulates user information associated with each Item instance.
Works by holding a reference to the owning Item and modifying its attributes.
"""
def __init__(self, data, parent):
#"parent" is needed for setting attrs
self.parent = parent
Expand All @@ -51,8 +91,17 @@ def __init__(self, data, parent):
self.addRating(data['rating'])
if 'wannto' in data.keys():
self.addRating(data['wantto'])

def addRating(self, rating:dict):
#trusts that these 4 keys will be provided: rating, comment, dateOf, faved
"""Add rating information from a properly formatted dict.
This trusts that the following 4 keys will be provided:
* rating
* comment
* dateOf
* faved
and modifies the owner.
"""
self.rating = rating
#set the parent's attrs to allow access
for key, val in self.rating.items():
Expand All @@ -64,17 +113,30 @@ def addRating(self, rating:dict):
month = dateOf['m'],
day = dateOf['d']
)

def addWantTo(self, wantto):
"""Add want-to-see information. Currently not used."""
self.wantto = wantto
#set the parent's attrs to allow access
for key, val in self.wantto.items():
self.parent.properties[key] = val

def hasRating(self):
return True if self.rating is not None else False

def hasWantTo(self):
return True if self.wantto is not None else False

def serialize(self):
#actually means "convert to dict", but whatever
"""Converts self into a compact dict representation.
If:
s = x.serialize()
y = UserData(s, x.parent)
then
x == y
shall always be true.
"""
serial = {}
if self.rating is not None:
serial['rating'] = self.rating
Expand All @@ -83,17 +145,18 @@ def serialize(self):
return serial

class BlueprintInheritance(type):
""" This metaclass ensures that all classes derived from Item have proper
access to all of the blueprints. Normally, iterating over cls.__dict__
only yields variables directly belonging to the given cls, so NONE of
the inherited class variables (including, most importantly, the most
basic Blueprints) will be visible. Thus, caching Blueprints and getting
presentation rules will not work in any derived classes, making them
useless.
The class constructor will perform all of said tasks (caching Blueprints,
preparing a presentation rules dict) during construction of the derived
classes, thus 1) enabling proper inheritance and 2) removing the need
to call cls.cacheBlueprints after creating every class.
"""Changes the way inheritance works for Blueprints. Crucial for Item class.
This metaclass ensures that all classes derived from Item have proper access
to all of the blueprints. Normally, iterating over cls.__dict__ only yields
variables directly belonging to the given cls, so NONE of the inherited class
variables (including, most importantly, the most basic Blueprints) will be
visible. Thus, caching Blueprints and getting presentation rules will not
work in any derived classes, making them useless.
The class constructor will perform all of said tasks (caching Blueprints,
preparing a presentation rules dict) during construction of the derived
classes, thus 1) enabling proper inheritance and 2) removing the need to call
cls.cacheBlueprints after creating every class.
"""
def __new__(cls, name, bases, dct):
# Explicitly create the new class
Expand All @@ -114,6 +177,33 @@ def __new__(cls, name, bases, dct):
return c

class Item(metaclass=BlueprintInheritance):
"""Base for all types of records used by Filmweb and in the program.
Item has a dual use: one as a class itself, another as an instance.
As a class, Item consists of a set of Blueprint instances, bound to it as
class attributes. These are used by the API to construct effective parsing
rules for each property (Blueprint) - see filmweb.py/FilmwebAPI.parseOne()
As an instance, Item is a collection of property data, stored as a dict
(self.properties) in which Blueprint attribute names serve as keys. This
allows easy extraction of actual data by names in two ways:
* by calling item['property'], which returns property data formatted into its
display format as defined by the corresponding Blueprint,
* by calling item.getRawProperty('property'), which returns the raw property.
Each Item instance can be "serialized" into a dict representation. Here, if:
x: Item
d = x.asDict()
y = Item(**d)
then it shall always be true that:
x == y
Special kind of non-storable properties is defined to allow separate
serialization of some properties, in this case the user data.
IMPORTANT: the Item class itself is never used directly (although it might).
Only its descendants, implementing specific details for each concrete type,
are ever used in the program.
"""
TYPE_STRING = ''
# Special ID field
id = Blueprint(
Expand Down Expand Up @@ -163,7 +253,7 @@ class Item(metaclass=BlueprintInheritance):
parsing={'tag':'div', 'class':'filmPreview__info--genres', 'text':True, 'list':True},
display=Blueprint._list
)
#rating fields are special, will be parsed and stored differently
# Rating fields are special, will be parsed and stored differently.
rating = Blueprint(
name='Moja ocena',
colwidth=150,
Expand Down Expand Up @@ -195,33 +285,41 @@ def __init__(self, userdata:dict={}, **properties):
self.properties[prop] = val
#construct the UserData object for rating/wantto information
self.userdata = UserData(userdata, self)

def __getitem__(self, prop):
"""Return a properly formatted value of a requested property."""
if prop in self.properties.keys():
val = self.properties[prop]
dsp = self.blueprints[prop].getDisplay()
return dsp(val)
else:
return ''

def getRawProperty(self, prop):
"""Return raw data of a requested property."""
if prop in self.properties.keys():
return self.properties[prop]
else:
return ''

def addRating(self, rating):
self.userdata.addRating(rating)

def addWantTo(self, wantto):
self.userdata.addWantTo(wantto)

def asDict(self):
#store all properties as dict, the exact reverse of __init__
"""Store all properties as dict, the exact reverse of __init__."""
_dict = {}
for prop in self.storables:
if prop in self.properties.keys():
_dict[prop] = self.properties[prop]
#also store the userdata
# Serialize the userdata separately.
_dict['userdata'] = self.userdata.serialize()
return _dict

class Movie(Item):
"""Item subclass specialized to hold Movie instances."""
TYPE_STRING = 'FILM'
duration = Blueprint(
name='Długość',
Expand Down Expand Up @@ -252,8 +350,12 @@ def __init__(self, userdata:dict={}, **properties):
super(Movie, self).__init__(userdata, **properties)

class Series(Movie):
"""Item subclass specialized to hold Series instances.
Everything is the same as with movies, except the duration field which has a
different meaning and thus can be named clearer.
"""
TYPE_STRING = 'SERIAL'
# Everything is the same as with movies, except the duration field can be name named clearer.
duration = Blueprint(
name='Dł. odc.',
colwidth=50,
Expand All @@ -265,8 +367,12 @@ def __init__(self, userdata:dict={}, **properties):
super(Series, self).__init__(userdata, **properties)

class Game(Item):
"""Item subclass specialized to hold Game instances.
Raw HTML representations of Games also have a countries div, but it has never
been observed to contain any data.
"""
TYPE_STRING = 'GRA'
# Games also have a countries div but it has never been observed to contain any data
developers = Blueprint(
name='Deweloper',
colwidth=150,
Expand Down

0 comments on commit a4d948e

Please sign in to comment.