def test_rich_comparison_and_len(self): # dicts for testing dict_0 = {} # Empty dictionary dict_1 = {1: 'Elephant'} # Single numeric key dict_2 = {1: 'Elephant', 2: 'Mouse'} # Multiple numeric keys # Construct NumDicts from dicts num_dict = NumDict() num_dict_0 = NumDict(dict_0) num_dict_1 = NumDict(dict_1) num_dict_2 = NumDict(dict_2) # Construct NumDicts from NumDicts num_dict_from_num_dict = NumDict(num_dict) num_dict_from_num_dict_0 = NumDict(num_dict_0) num_dict_from_num_dict_1 = NumDict(num_dict_1) num_dict_from_num_dict_2 = NumDict(num_dict_2) all_dicts = [ dict_0, dict_1, dict_2, num_dict, num_dict_0, num_dict_1, num_dict_2, num_dict_from_num_dict, num_dict_from_num_dict_0, num_dict_from_num_dict_1, num_dict_from_num_dict_2 ] for val_a in all_dicts: for val_b in all_dicts: self.assertEqual(val_a == val_b, len(val_a) == len(val_b))
class Overview(object): UNAIRED = UNAIRED # 1 SNATCHED = SNATCHED # 2 WANTED = WANTED # 3 GOOD = DOWNLOADED # 4 SKIPPED = SKIPPED # 5 SNATCHED_PROPER = SNATCHED_PROPER # 9 SNATCHED_BEST = SNATCHED_BEST # 12 # Should suffice! QUAL = 50 overviewStrings = NumDict({ SKIPPED: "skipped", WANTED: "wanted", QUAL: "qual", GOOD: "good", UNAIRED: "unaired", SNATCHED: "snatched", # we can give these a different class later, otherwise # breaks checkboxes in displayShow for showing different statuses SNATCHED_BEST: "snatched", SNATCHED_PROPER: "snatched" })
class Quality(object): """Determine quality and set status codes.""" NONE = 0 # 0 SDTV = 1 # 1 SDDVD = 1 << 1 # 2 HDTV = 1 << 2 # 4 RAWHDTV = 1 << 3 # 8 -- 720p/1080i mpeg2 (trollhd releases) FULLHDTV = 1 << 4 # 16 -- 1080p HDTV (QCF releases) HDWEBDL = 1 << 5 # 32 FULLHDWEBDL = 1 << 6 # 64 -- 1080p web-dl HDBLURAY = 1 << 7 # 128 FULLHDBLURAY = 1 << 8 # 256 UHD_4K_TV = 1 << 9 # 512 -- 2160p aka 4K UHD aka UHD-1 UHD_4K_WEBDL = 1 << 10 # 1024 UHD_4K_BLURAY = 1 << 11 # 2048 UHD_8K_TV = 1 << 12 # 4096 -- 4320p aka 8K UHD aka UHD-2 UHD_8K_WEBDL = 1 << 13 # 8192 UHD_8K_BLURAY = 1 << 14 # 16384 ANYHDTV = HDTV | FULLHDTV # 20 ANYWEBDL = HDWEBDL | FULLHDWEBDL # 96 ANYBLURAY = HDBLURAY | FULLHDBLURAY # 384 # put these bits at the other end of the spectrum, # far enough out that they shouldn't interfere UNKNOWN = 1 << 15 # 32768 qualityStrings = NumDict({ None: "None", NONE: "N/A", UNKNOWN: "Unknown", SDTV: "SDTV", SDDVD: "SD DVD", HDTV: "720p HDTV", RAWHDTV: "RawHD", FULLHDTV: "1080p HDTV", HDWEBDL: "720p WEB-DL", FULLHDWEBDL: "1080p WEB-DL", HDBLURAY: "720p BluRay", FULLHDBLURAY: "1080p BluRay", UHD_4K_TV: "4K UHD TV", UHD_8K_TV: "8K UHD TV", UHD_4K_WEBDL: "4K UHD WEB-DL", UHD_8K_WEBDL: "8K UHD WEB-DL", UHD_4K_BLURAY: "4K UHD BluRay", UHD_8K_BLURAY: "8K UHD BluRay", }) sceneQualityStrings = NumDict({ None: "None", NONE: "N/A", UNKNOWN: "Unknown", SDTV: "", SDDVD: "", HDTV: "720p", RAWHDTV: "1080i", FULLHDTV: "1080p", HDWEBDL: "720p", FULLHDWEBDL: "1080p", HDBLURAY: "720p BluRay", FULLHDBLURAY: "1080p BluRay", UHD_4K_TV: "2160p", UHD_8K_TV: "4320p", UHD_4K_WEBDL: "2160p", UHD_8K_WEBDL: "4320p", UHD_4K_BLURAY: "2160p BluRay", UHD_8K_BLURAY: "4320p BluRay", }) combinedQualityStrings = NumDict({ ANYHDTV: "HDTV", ANYWEBDL: "WEB-DL", ANYBLURAY: "BluRay" }) cssClassStrings = NumDict({ None: "None", NONE: "N/A", UNKNOWN: "Unknown", SDTV: "SDTV", SDDVD: "SDDVD", HDTV: "HD720p", RAWHDTV: "RawHD", FULLHDTV: "HD1080p", HDWEBDL: "HD720p", FULLHDWEBDL: "HD1080p", HDBLURAY: "HD720p", FULLHDBLURAY: "HD1080p", UHD_4K_TV: "UHD-4K", UHD_8K_TV: "UHD-8K", UHD_4K_WEBDL: "UHD-4K", UHD_8K_WEBDL: "UHD-8K", UHD_4K_BLURAY: "UHD-4K", UHD_8K_BLURAY: "UHD-8K", ANYHDTV: "any-hd", ANYWEBDL: "any-hd", ANYBLURAY: "any-hd" }) statusPrefixes = NumDict({ DOWNLOADED: "Downloaded", SNATCHED: "Snatched", SNATCHED_PROPER: "Snatched (Proper)", FAILED: "Failed", SNATCHED_BEST: "Snatched (Best)", ARCHIVED: "Archived" }) @staticmethod def _get_status_strings(status): """ Return string values associated with Status prefix. :param status: Status prefix to resolve :return: Human readable status value """ to_return = {} for quality in Quality.qualityStrings: if quality is not None: stat = Quality.statusPrefixes[status] qual = Quality.qualityStrings[quality] comp = Quality.composite_status(status, quality) to_return[comp] = '%s (%s)' % (stat, qual) return to_return @staticmethod def combine_qualities(allowed_qualities, preferred_qualities): any_quality = 0 best_quality = 0 if allowed_qualities: any_quality = reduce(operator.or_, allowed_qualities) if preferred_qualities: best_quality = reduce(operator.or_, preferred_qualities) return any_quality | (best_quality << 16) @staticmethod def split_quality(quality): if quality is None: quality = Quality.NONE allowed_qualities = [] preferred_qualities = [] for cur_qual in Quality.qualityStrings: if cur_qual is None: cur_qual = Quality.NONE if cur_qual & quality: allowed_qualities.append(cur_qual) if cur_qual << 16 & quality: preferred_qualities.append(cur_qual) return sorted(allowed_qualities), sorted(preferred_qualities) @staticmethod def name_quality(name, anime=False, extend=True): """ Return The quality from an episode File renamed by the application. If no quality is achieved it will try scene_quality regex :param name: to parse :param anime: Boolean to indicate if the show we're resolving is Anime :param extend: boolean to extend methods to try :return: Quality prefix """ # Try Scene names first quality = Quality.scene_quality(name, anime) if quality != Quality.UNKNOWN: return quality # Additional methods to get quality should be added here if extend: return Quality._extend_quality(name) return Quality.UNKNOWN @staticmethod def scene_quality(name, anime=False): """ Return The quality from the Scene episode File. :param name: Episode filename to analyse :param anime: Boolean to indicate if the show we're resolving is Anime :return: Quality """ from .tagger.episode import EpisodeTags if not name: return Quality.UNKNOWN else: name = path.basename(name) result = None ep = EpisodeTags(name) if anime: sd_options = tags.anime_sd.search(name) hd_options = tags.anime_hd.search(name) full_hd = tags.anime_fullhd.search(name) ep.rex[u'bluray'] = tags.anime_bluray # BluRay if ep.bluray and (full_hd or hd_options): result = Quality.FULLHDBLURAY if full_hd else Quality.HDBLURAY # HD TV elif not ep.bluray and (full_hd or hd_options): result = Quality.FULLHDTV if full_hd else Quality.HDTV # SD DVD elif ep.dvd: result = Quality.SDDVD # SD TV elif sd_options: result = Quality.SDTV return Quality.UNKNOWN if result is None else result # Is it UHD? if ep.vres in [2160, 4320] and ep.scan == u'p': # BluRay full_res = (ep.vres == 4320) if ep.avc and ep.bluray: result = Quality.UHD_4K_BLURAY if not full_res else Quality.UHD_8K_BLURAY # WEB-DL elif (ep.avc and ep.itunes) or ep.web: result = Quality.UHD_4K_WEBDL if not full_res else Quality.UHD_8K_WEBDL # HDTV elif ep.avc and ep.tv == u'hd': result = Quality.UHD_4K_TV if not full_res else Quality.UHD_8K_TV # Is it HD? elif ep.vres in [1080, 720]: if ep.scan == u'p': # BluRay full_res = (ep.vres == 1080) if ep.avc and (ep.bluray or ep.hddvd): result = Quality.FULLHDBLURAY if full_res else Quality.HDBLURAY # WEB-DL elif (ep.avc and ep.itunes) or ep.web: result = Quality.FULLHDWEBDL if full_res else Quality.HDWEBDL # HDTV elif ep.avc and ep.tv == u'hd': result = Quality.FULLHDTV if full_res else Quality.HDTV elif all([ep.vres == 720, ep.tv == u'hd', ep.mpeg]): result = Quality.RAWHDTV elif (ep.res == u'1080i') and ep.tv == u'hd': if ep.mpeg or (ep.raw and ep.avc_non_free): result = Quality.RAWHDTV elif ep.hrws: result = Quality.HDTV # Is it SD? elif ep.xvid or ep.avc: # Is it aussie p2p? If so its 720p if all([ep.tv == u'hd', ep.widescreen, ep.aussie]): result = Quality.HDTV # SD DVD elif ep.dvd or ep.bluray: result = Quality.SDDVD # SDTV elif ep.res == u'480p' or any([ep.tv, ep.sat, ep.web]): result = Quality.SDTV return Quality.UNKNOWN if result is None else result @staticmethod def _extend_quality(file_path): """ Try other methods to get the file quality. :param file_path: File path of episode to analyse :return: Quality prefix """ quality = Quality.quality_from_file_meta(file_path) if quality != Quality.UNKNOWN: return quality # This assumes that any .ts file is RAWHDTV (probably wrong) if file_path.lower().endswith('.ts'): return Quality.RAWHDTV return Quality.UNKNOWN @staticmethod def quality_from_file_meta(file_path): """ Get quality file file metadata. :param file_path: File path to analyse :return: Quality prefix """ if not os.path.isfile(file_path): return Quality.UNKNOWN knowledge = knowit.know(file_path) if not knowledge: return Quality.UNKNOWN height = None for track in knowledge.get('video') or []: height = track.get('height') if height: break if not height: return Quality.UNKNOWN height = int(height.magnitude) # TODO: Use knowledge information like 'resolution' base_filename = path.basename(file_path) bluray = re.search(r"blue?-?ray|hddvd|b[rd](rip|mux)", base_filename, re.I) is not None webdl = re.search(r"web.?dl|web(rip|mux|hd)", base_filename, re.I) is not None ret = Quality.UNKNOWN if 3240 < height: ret = ((Quality.UHD_8K_TV, Quality.UHD_8K_BLURAY)[bluray], Quality.UHD_8K_WEBDL)[webdl] if 1620 < height <= 3240: ret = ((Quality.UHD_4K_TV, Quality.UHD_4K_BLURAY)[bluray], Quality.UHD_4K_WEBDL)[webdl] elif 800 < height <= 1620: ret = ((Quality.FULLHDTV, Quality.FULLHDBLURAY)[bluray], Quality.FULLHDWEBDL)[webdl] elif 680 < height <= 800: ret = ((Quality.HDTV, Quality.HDBLURAY)[bluray], Quality.HDWEBDL)[webdl] elif height <= 680: ret = (Quality.SDTV, Quality.SDDVD)[re.search( r'dvd|b[rd]rip|blue?-?ray', base_filename, re.I) is not None] return ret composite_status_quality = namedtuple('composite_status', ['status', 'quality']) @staticmethod def composite_status(status, quality): if quality is None: quality = Quality.NONE return status + 100 * quality @staticmethod def quality_downloaded(status): return (status - DOWNLOADED) / 100 @staticmethod def split_composite_status(status): """ Split a composite status code into a status and quality. :param status: to split :returns: a namedtuple containing (status, quality) """ status = long(status) if status == UNKNOWN: return Quality.composite_status_quality(UNKNOWN, Quality.UNKNOWN) for q in sorted(Quality.qualityStrings.keys(), reverse=True): if status > q * 100: return Quality.composite_status_quality(status - q * 100, q) return Quality.composite_status_quality(status, Quality.NONE) @staticmethod def scene_quality_from_name(name, quality): """ Get Scene naming parameters from filename and quality. :param name: filename to check :type name: text_type :param quality: int of quality to make sure we get the right release type :type quality: int :return: release type and/or encoder type for scene quality naming :rtype: text_type """ rel_type = '' name = name.lower() codec = re.search(r'[xh].?26[45]', name) or '' if codec and codec.group(0).endswith('4') or 'avc' in name: if codec and codec.group(0).startswith('h'): codec = ' h264' else: codec = ' x264' elif codec and codec.group(0).endswith('5') or 'hevc' in name: if codec and codec.group(0).startswith('h'): codec = ' h265' else: codec = ' x265' elif 'xvid' in name: codec = ' XviD' elif 'divx' in name: codec = ' DivX' # If any HDTV type or SDTV if quality in (1, 4, 8, 16, 512, 4096): rel_type = ' HDTV' if 'ahdtv' in name: rel_type = ' AHDTV' elif 'hr.pdtv' in name: rel_type = ' HR.PDTV' elif 'pdtv' in name: rel_type = ' PDTV' elif 'satrip' in name: rel_type = ' SATRip' elif 'dsr' in name: rel_type = ' DSR' elif 'uhdtv' in name: rel_type = ' UHDTV' # If SDDVD if quality == 2: rel_type = ' BDRip' if re.search(r'br(-| |\.)?(rip|mux)', name): rel_type = ' BRRip' elif re.search(r'dvd(-| |\.)?(rip|mux)', name): rel_type = ' DVDRip' # If any WEB type if quality in (32, 64, 1024, 8192): rel_type = ' WEB' if re.search(r'web(-| |\.)?dl', name): rel_type = ' WEB-DL' elif re.search(r'web(-| |\.)?(rip|mux)', name): rel_type = ' WEBRip' return rel_type + codec @staticmethod def status_from_name(name, anime=False): """ Get a status object from filename. :param name: Filename to check :param anime: boolean to enable anime parsing :return: Composite status/quality object """ quality = Quality.name_quality(name, anime) return Quality.composite_status(DOWNLOADED, quality) guessit_map = { '720p': { 'HDTV': HDTV, 'WEB-DL': HDWEBDL, 'WEBRip': HDWEBDL, 'BluRay': HDBLURAY, }, '1080i': RAWHDTV, '1080p': { 'HDTV': FULLHDTV, 'WEB-DL': FULLHDWEBDL, 'WEBRip': FULLHDWEBDL, 'BluRay': FULLHDBLURAY }, '4K': { 'HDTV': UHD_4K_TV, 'WEB-DL': UHD_4K_WEBDL, 'WEBRip': UHD_4K_WEBDL, 'BluRay': UHD_4K_BLURAY } } to_guessit_format_list = [ ANYHDTV, ANYWEBDL, ANYBLURAY, ANYHDTV | UHD_4K_TV, ANYWEBDL | UHD_4K_WEBDL, ANYBLURAY | UHD_4K_BLURAY ] to_guessit_screen_size_map = { HDTV | HDWEBDL | HDBLURAY: '720p', RAWHDTV: '1080i', FULLHDTV | FULLHDWEBDL | FULLHDBLURAY: '1080p', UHD_4K_TV | UHD_4K_WEBDL | UHD_4K_BLURAY: '4K', } @staticmethod def should_search(status, show_obj, manually_searched): """Return true if that episodes should be search for a better quality. If cur_quality is Quality.NONE, it will return True as its a invalid quality If cur_quality is Quality.UNKNOWN it will return True only if is not in Allowed (Unknown can be in Allowed) :param status: current status of the episode :param show_obj: Series object of the episode we will check if we should search or not :param manually_searched: if episode was manually searched by user :return: True if need to run a search for given episode """ cur_status, cur_quality = Quality.split_composite_status( int(status) or UNKNOWN) allowed_qualities, preferred_qualities = show_obj.current_qualities # When user manually searched, we should consider this as final quality. if manually_searched: return False, 'Episode was manually searched. Skipping episode' # Can't be SNATCHED_BEST because the quality is already final (unless user changes qualities). # All other status will return false: IGNORED, SKIPPED, FAILED. if cur_status not in (WANTED, DOWNLOADED, SNATCHED, SNATCHED_PROPER, SNATCHED_BEST): return False, 'Status is not allowed: {0}. Skipping episode'.format( statusStrings[cur_status]) # If current status is WANTED, we must always search if cur_status != WANTED: if cur_quality not in allowed_qualities + preferred_qualities: return True, 'Quality is not in Allowed|Preferred. Searching episode' elif preferred_qualities: if cur_quality in preferred_qualities: return False, 'Quality is already Preferred. Skipping episode' else: return True, 'Quality is not Preferred. Searching episode' elif cur_quality in allowed_qualities: return False, 'Quality is already Allowed. Skipping episode' else: return True, 'Status is WANTED. Searching episode' return False, 'No rule set to allow the search' @staticmethod def should_replace(ep_status, old_quality, new_quality, allowed_qualities, preferred_qualities, download_current_quality=False, force=False, manually_searched=False, search_type=None): """Return true if the old quality should be replaced with new quality. If not preferred qualities, then any downloaded quality is final if preferred quality, then new quality should be higher than existing one AND not be in preferred If new quality is already in preferred then is already final quality. Force (forced search) bypass episode status only or unknown quality If old quality is Quality.NONE, it will be replaced :param ep_status: current status of the episode :param old_quality: current quality of the episode :param new_quality: quality of the episode we found it and check if we should snatch it :param allowed_qualities: List of selected allowed qualities of the show we are checking :param preferred_qualities: List of selected preferred qualities of the show we are checking :param download_current_quality: True if user wants the same existing quality to be snatched :param force: True if user did a forced search for that episode :param manually_searched: True if episode was manually searched by user :param search_type: The search type, that started this method :return: True if the old quality should be replaced with new quality. """ if ep_status and ep_status not in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER: if not force: return False, 'Episode status is not DOWNLOADED|SNATCHED|SNATCHED PROPER. Ignoring new quality' # If existing quality is UNKNOWN but Preferred is set, UNKNOWN should be replaced. if old_quality == Quality.UNKNOWN: if not (force or preferred_qualities): return False, 'Existing quality is UNKNOWN. Ignoring new quality' if manually_searched: if not force: # We only allow replace a manual searched episode if is a forced search return False, 'Existing episode quality was manually snatched. Ignoring all new qualities' if not Quality.wanted_quality(new_quality, allowed_qualities, preferred_qualities): return False, 'New quality is not in any wanted quality lists. Ignoring new quality' if search_type == PROPER_SEARCH: if new_quality == old_quality: return True, 'New quality is the same as the existing quality. Accepting PROPER' return False, 'New quality is different from the existing quality.' \ 'Ignoring PROPER, as we only PROPER the same release.' if old_quality not in allowed_qualities + preferred_qualities: # If old quality is no longer wanted quality and new quality is wanted, we should replace. return True, 'Existing quality is no longer in any wanted quality lists. Accepting new quality' if force and download_current_quality: # If we already downloaded quality, just redownload it as long is still part of the wanted qualities return new_quality == old_quality, 'Redownloading same quality' if preferred_qualities: # Don't replace because old quality is already best quality. if old_quality in preferred_qualities: return False, 'Existing quality is already a preferred quality. Ignoring new quality' # Old quality is not final. Check if we should replace: # Replace if preferred quality if new_quality in preferred_qualities: return True, 'New quality is preferred. Accepting new quality' return False, 'New quality is same/lower quality (and not preferred). Ignoring new quality' else: # Allowed quality should never be replaced return False, 'Existing quality is already final (allowed only). Ignoring new quality' @staticmethod def is_higher_quality(current_quality, new_quality, allowed_qualities, preferred_qualities): """Check is new quality is better than current quality based on allowed and preferred qualities.""" if new_quality in preferred_qualities: return new_quality > current_quality elif new_quality in allowed_qualities: if current_quality in preferred_qualities: return False return new_quality > current_quality @staticmethod def wanted_quality(new_quality, allowed_qualities, preferred_qualities): """Check if new quality is wanted.""" return new_quality in allowed_qualities + preferred_qualities @staticmethod def from_guessit(guess): """ Return a Quality from a guessit dict. :param guess: guessit dict :type guess: dict :return: quality :rtype: int """ screen_size = guess.get('screen_size') fmt = guess.get('format') if not screen_size or isinstance(screen_size, list): return Quality.UNKNOWN format_map = Quality.guessit_map.get(screen_size) if not format_map: return Quality.UNKNOWN if isinstance(format_map, int): return format_map if not fmt or isinstance(fmt, list): return Quality.UNKNOWN quality = format_map.get(fmt) return quality if quality is not None else Quality.UNKNOWN @staticmethod def to_guessit(status): """Return a guessit dict containing 'screen_size and format' from a Quality (composite status). :param status: a quality composite status :type status: int :return: dict {'screen_size': <screen_size>, 'format': <format>} :rtype: dict (str, str) """ _, quality = Quality.split_composite_status(status) screen_size = Quality.to_guessit_screen_size(quality) fmt = Quality.to_guessit_format(quality) result = dict() if screen_size: result['screen_size'] = screen_size if fmt: result['format'] = fmt return result @staticmethod def to_guessit_format(quality): """Return a guessit format from a Quality. :param quality: the quality :type quality: int :return: guessit format :rtype: str """ for q in Quality.to_guessit_format_list: if quality & q: key = q & ( 512 - 1 ) # 4k formats are bigger than 384 and are not part of ANY* bit set return Quality.combinedQualityStrings.get(key) @staticmethod def to_guessit_screen_size(quality): """Return a guessit screen_size from a Quality. :param quality: the quality :type quality: int :return: guessit screen_size :rtype: str """ for key, value in Quality.to_guessit_screen_size_map.items(): if quality & key: return value DOWNLOADED = None SNATCHED = None SNATCHED_PROPER = None FAILED = None SNATCHED_BEST = None ARCHIVED = None
# Notification Types NOTIFY_SNATCH = 1 NOTIFY_DOWNLOAD = 2 NOTIFY_SUBTITLE_DOWNLOAD = 3 NOTIFY_GIT_UPDATE = 4 NOTIFY_GIT_UPDATE_TEXT = 5 NOTIFY_LOGIN = 6 NOTIFY_LOGIN_TEXT = 7 NOTIFY_SNATCH_PROPER = 8 notifyStrings = NumDict({ NOTIFY_SNATCH: "Started Download", NOTIFY_DOWNLOAD: "Download Finished", NOTIFY_SUBTITLE_DOWNLOAD: "Subtitle Download Finished", NOTIFY_GIT_UPDATE: "Medusa Updated", NOTIFY_GIT_UPDATE_TEXT: "Medusa Updated To Commit#: ", NOTIFY_LOGIN: "******", NOTIFY_LOGIN_TEXT: "New login from IP: {0}. http://geomaplookup.net/?ip={0}", NOTIFY_SNATCH_PROPER: "Started PROPER Download" }) # Episode statuses UNKNOWN = -1 # should never happen UNAIRED = 1 # episodes that haven't aired yet SNATCHED = 2 # qualified with quality WANTED = 3 # episodes we don't have but want to get DOWNLOADED = 4 # qualified with quality SKIPPED = 5 # episodes we don't want ARCHIVED = 6 # non-local episodes (counts toward download completion stats) IGNORED = 7 # episodes that you don't want included in your download stats
def __init__(self): # An instance variable __missing__ should have no effect self.__missing__ = lambda key: None NumDict.__init__(self)
def test_dict_access_and_mod(self): # pylint: disable=too-many-locals, too-many-statements # dicts for testing dict_0 = {} # Empty dictionary dict_1 = {1: 'Elephant'} # Single numeric key dict_2 = {1: 'Elephant', 2: 'Mouse'} # Multiple numeric keys # Construct NumDicts from dicts num_dict_0 = NumDict() num_dict_1 = NumDict(dict_1) num_dict_2 = NumDict(dict_2) # test __getitem__ self.assertEqual(num_dict_2[1], 'Elephant') with self.assertRaises(KeyError): print(num_dict_1['Mouse']) # key is not numeric with self.assertRaises(KeyError): num_dict_1.__getitem__('Mouse') # key is not numeric with self.assertRaises(KeyError): print(num_dict_1[None]) # key does not exist with self.assertRaises(KeyError): num_dict_1.__getitem__(None) # key does not exist # Test __setitem__ num_dict_3 = NumDict(num_dict_2) self.assertEqual(num_dict_2, num_dict_3) num_dict_3[2] = 'Frog' self.assertNotEqual(num_dict_2, num_dict_3) # Check None keys and numeric key conversion num_dict_3['3'] = 'Armadillo' num_dict_3[None] = 'Cockroach' # Check long ints num_dict_3[12390809518259081208909880312] = 'Squid' num_dict_3['12390809518259081208909880312'] = 'Octopus' self.assertEqual(num_dict_3[12390809518259081208909880312], 'Octopus') with self.assertRaises(TypeError): num_dict_3.__setitem__('Gorilla', 1) # key is not numeric with self.assertRaises(TypeError): num_dict_3['Chimpanzee'] = 1 # key is not numeric with self.assertRaises(TypeError): num_dict_3[(4, 1)] = 1 # key is not numeric with self.assertRaises(TypeError): num_dict_3[[1, 3, 4]] = 1 # key is not numeric and is not hashable # Test __delitem__ del num_dict_3[3] del num_dict_3[None] with self.assertRaises(KeyError): del num_dict_3[3] # already deleted with self.assertRaises(KeyError): num_dict_3.__delitem__(3) # already deleted with self.assertRaises(KeyError): del num_dict_3[ 'Mouse'] # key would not exist, since it is not numeric # Test clear num_dict_3.clear() self.assertEqual(num_dict_3, {}) # Test copy() num_dict_2a = dict_2.copy() self.assertEqual(num_dict_2, num_dict_2a) num_dict_2b = num_dict_2.copy() self.assertEqual(num_dict_2b, num_dict_2) num_dict_2c = UserDict({1: 'Elephant', 2: 'Mouse'}) num_dict_2d = num_dict_2c.copy( ) # making a copy of a UserDict is special cased self.assertEqual(num_dict_2c, num_dict_2d) class MyNumDict(NumDict): """Subclass Numdict for testing.""" def display(self): """Add a method to subclass to differentiate from superclass.""" print('MyNumDict:', self) my_num_dict = MyNumDict(num_dict_2) my_num_dict_a = my_num_dict.copy() self.assertEqual(my_num_dict_a, my_num_dict) my_num_dict[1] = 'Frog' self.assertNotEqual(my_num_dict_a, my_num_dict) # Test keys, items, values self.assertEqual(sorted(num_dict_2.keys()), sorted(dict_2.keys())) self.assertEqual(sorted(num_dict_2.items()), sorted(dict_2.items())) self.assertEqual(sorted(num_dict_2.values()), sorted(dict_2.values())) # Test "in". for i in num_dict_2: self.assertIn(i, num_dict_2) self.assertEqual(i in num_dict_1, i in dict_1) self.assertEqual(i in num_dict_0, i in dict_0) self.assertFalse(None in num_dict_2) self.assertEqual(None in num_dict_2, None in dict_2) dict_2[None] = 'Cow' num_dict_2[None] = dict_2[None] self.assertTrue(None in num_dict_2) self.assertEqual(None in num_dict_2, None in dict_2) self.assertFalse('Penguin' in num_dict_2) # Test update test = NumDict() test.update(dict_2) self.assertEqual(test, num_dict_2) # Test get for i in num_dict_2: self.assertEqual(num_dict_2.get(i), num_dict_2[i]) self.assertEqual(num_dict_1.get(i), dict_1.get(i)) self.assertEqual(num_dict_0.get(i), dict_0.get(i)) for i in ['purple', None, 12312301924091284, 23]: self.assertEqual(num_dict_2.get(i), dict_2.get(i), i) with self.assertRaises(AssertionError): i = '1' self.assertEqual( num_dict_2.get(i), dict_2.get(i), i) # dict_2 expects string key which does not exist # Test "in" iteration. num_dict_2b = num_dict_2 for i in range(20): num_dict_2[i] = str(i) num_dict_2b[str(i)] = str(i) self.assertEqual(num_dict_2, num_dict_2b) ikeys = [] for k in num_dict_2: ikeys.append(k) self.assertEqual(set(ikeys), set(num_dict_2.keys())) # Test setdefault val = 1 test = NumDict() self.assertEqual(test.setdefault(val, 42), 42) self.assertEqual(test.setdefault(val, '42'), 42) self.assertNotEqual(test.setdefault(val, 42), '42') self.assertNotEqual(test.setdefault(val, '42'), '42') self.assertIn(val, test) self.assertEqual(test.setdefault(val, 23), 42) self.assertEqual(test.setdefault(val, '23'), 42) self.assertNotEqual(test.setdefault(val, 23), '42') self.assertNotEqual(test.setdefault(val, '23'), '42') self.assertIn(val, test) # Test pop val = 1 test = NumDict({val: 42}) self.assertEqual(test.pop(val), 42) self.assertRaises(KeyError, test.pop, val) self.assertEqual(test.pop(val, 1), 1) test[val] = 42 self.assertEqual(test.pop(val, 1), 42) # Test popitem val = 1 test = NumDict({val: 42}) self.assertEqual(test.popitem(), (val, 42)) self.assertRaises(KeyError, test.popitem)
def test_constructors(self): # pylint: disable=too-many-locals, too-many-statements # dicts for testing dict_0 = {} # Empty dictionary dict_1 = {1: 'Elephant'} # Single numeric key dict_2 = {1: 'Elephant', 2: 'Mouse'} # Multiple numeric keys dict_3 = {'3': 'Aardvark'} # Numeric string key dict_4 = {'3': 'Aardvark', '4': 'Ant'} # Multiple numeric string keys dict_5 = { 5: 'Cat', '6': 'Dog' } # Mixed numeric and numeric string keys dict_6 = {1: None, '2': None} # None as values dict_7 = {None: 'Empty'} # None as key # Construct NumDicts from dicts num_dict = NumDict() num_dict_0 = NumDict(dict_0) num_dict_1 = NumDict(dict_1) num_dict_2 = NumDict(dict_2) num_dict_3 = NumDict(dict_3) num_dict_4 = NumDict(dict_4) num_dict_5 = NumDict(dict_5) num_dict_6 = NumDict(dict_6) num_dict_7 = NumDict(dict_7) # Most NumDicts from dicts should compare equal... self.assertEqual(num_dict, {}) self.assertEqual(num_dict_0, dict_0) self.assertEqual(num_dict_1, dict_1) self.assertEqual(num_dict_2, dict_2) # ...however, numeric keys are not equal to numeric string keys... self.assertNotEqual(num_dict_3, dict_3) self.assertNotEqual(num_dict_4, dict_4) self.assertNotEqual(num_dict_5, dict_5) self.assertNotEqual(num_dict_6, dict_6) # ...but None keys work just fine self.assertEqual(num_dict_7, dict_7) # Construct dicts from NumDicts dict_from_num_dict = dict(num_dict) dict_from_num_dict_1 = dict(num_dict_1) dict_from_num_dict_2 = dict(num_dict_2) dict_from_num_dict_3 = dict(num_dict_3) dict_from_num_dict_4 = dict(num_dict_4) dict_from_num_dict_5 = dict(num_dict_5) dict_from_num_dict_6 = dict(num_dict_6) dict_from_num_dict_7 = dict(num_dict_7) # All dicts from NumDicts should compare equal self.assertEqual(num_dict, dict_from_num_dict) self.assertEqual(num_dict_1, dict_from_num_dict_1) self.assertEqual(num_dict_2, dict_from_num_dict_2) self.assertEqual(num_dict_3, dict_from_num_dict_3) self.assertEqual(num_dict_4, dict_from_num_dict_4) self.assertEqual(num_dict_5, dict_from_num_dict_5) self.assertEqual(num_dict_6, dict_from_num_dict_6) self.assertEqual(num_dict_7, dict_from_num_dict_7) # Construct NumDicts from NumDicts num_dict_from_num_dict = NumDict(num_dict) num_dict_from_num_dict_0 = NumDict(num_dict_0) num_dict_from_num_dict_1 = NumDict(num_dict_1) num_dict_from_num_dict_2 = NumDict(num_dict_2) num_dict_from_num_dict_3 = NumDict(num_dict_3) num_dict_from_num_dict_4 = NumDict(num_dict_4) num_dict_from_num_dict_5 = NumDict(num_dict_5) num_dict_from_num_dict_6 = NumDict(num_dict_6) num_dict_from_num_dict_7 = NumDict(num_dict_7) # All NumDicts from NumDicts should compare equal self.assertEqual(num_dict, num_dict_from_num_dict) self.assertEqual(num_dict_0, num_dict_from_num_dict_0) self.assertEqual(num_dict_1, num_dict_from_num_dict_1) self.assertEqual(num_dict_2, num_dict_from_num_dict_2) self.assertEqual(num_dict_3, num_dict_from_num_dict_3) self.assertEqual(num_dict_4, num_dict_from_num_dict_4) self.assertEqual(num_dict_5, num_dict_from_num_dict_5) self.assertEqual(num_dict_6, num_dict_from_num_dict_6) self.assertEqual(num_dict_7, num_dict_from_num_dict_7) # keyword arg constructor should fail with self.assertRaises(TypeError): NumDict( one=1, two=2) # Raise TypeError since we can't have numeric keywords # item sequence constructors work fine... self.assertEqual(NumDict([(1, 'Elephant'), (2, 'Mouse')]), dict_from_num_dict_2) self.assertEqual(NumDict(dict=[(1, 'Elephant'), (2, 'Mouse')]), dict_from_num_dict_2) self.assertEqual(NumDict([(1, 'Elephant'), ('2', 'Mouse')]), dict_from_num_dict_2) self.assertEqual(NumDict(dict=[('1', 'Elephant'), (2, 'Mouse')]), dict_from_num_dict_2) # ...unless you have a non-numeric key with self.assertRaises(TypeError): NumDict([('Rat', 11), ('Snake', 12)]) with self.assertRaises(TypeError): NumDict(dict=[('Rat', 11), ('Snake', 12)]) # combining item sequence constructors with keyword args does not work with self.assertRaises( TypeError ): # Raise TypeError since we can't have numeric keywords NumDict([(1, 'one'), (2, 'two')], two=3, five=4) # alternate constructors dict_8 = {1: 'Echo', 2: 'Echo'} self.assertEqual(NumDict.fromkeys('1 2'.split()), dict_from_num_dict_6) self.assertEqual(NumDict().fromkeys('1 2'.split()), dict_from_num_dict_6) self.assertEqual(NumDict.fromkeys('1 2'.split(), 'Echo'), dict_8) self.assertEqual(NumDict().fromkeys('1 2'.split(), 'Echo'), dict_8) self.assertTrue(num_dict_1.fromkeys('1 2'.split()) is not num_dict_1) self.assertIsInstance(num_dict_1.fromkeys('1 2'.split()), NumDict) self.assertIsInstance(num_dict_2.fromkeys('1 2'.split()), NumDict) self.assertIsInstance(num_dict_3.fromkeys('1 2'.split()), NumDict) self.assertIsInstance(num_dict_4.fromkeys('1 2'.split()), NumDict)
def test_repr(self): # pylint: disable=too-many-locals # dicts for testing dict_0 = {} # Empty dictionary dict_1 = {1: 'Elephant'} # Single numeric key dict_2 = {1: 'Elephant', 2: 'Mouse'} # Multiple numeric keys dict_3 = {'3': 'Aardvark'} # Numeric string key dict_4 = {'3': 'Aardvark', '4': 'Ant'} # Multiple numeric string keys dict_5 = { 5: 'Cat', '6': 'Dog' } # Mixed numeric and numeric string keys dict_6 = {1: None, '2': None} # None as values dict_7 = {None: 'Empty'} # None as key # Construct NumDicts from dicts num_dict = NumDict() num_dict_0 = NumDict(dict_0) num_dict_1 = NumDict(dict_1) num_dict_2 = NumDict(dict_2) num_dict_3 = NumDict(dict_3) num_dict_4 = NumDict(dict_4) num_dict_5 = NumDict(dict_5) num_dict_6 = NumDict(dict_6) num_dict_7 = NumDict(dict_7) reps = ( "{}", "{1: 'Elephant'}", "{1: 'Elephant', 2: 'Mouse'}", "'3': 'Aardvark'", "{'3': 'Aardvark', '4': 'Ant'}", "{5: 'Cat', '6': 'Dog'}", "{1: None, '2': None}", "{None: 'Empty'}", ) # Most representations of NumDicts should compare equal to dicts... self.assertEqual(str(num_dict), str({})) self.assertEqual(repr(num_dict), repr({})) self.assertIn(repr(num_dict), reps) self.assertEqual(str(num_dict_0), str(dict_0)) self.assertEqual(repr(num_dict_0), repr(dict_0)) self.assertIn(repr(num_dict_0), reps) self.assertEqual(str(num_dict_1), str(dict_1)) self.assertEqual(repr(num_dict_1), repr(dict_1)) self.assertIn(repr(num_dict_1), reps) self.assertEqual(str(num_dict_2), str(dict_2)) self.assertEqual(repr(num_dict_2), repr(dict_2)) self.assertIn(repr(num_dict_2), reps) # ...however, numeric keys are not equal to numeric string keys... # ...so the string representations for those are different... self.assertNotEqual(str(num_dict_3), str(dict_3)) self.assertNotEqual(repr(num_dict_3), repr(dict_3)) self.assertNotIn(repr(num_dict_3), reps) self.assertNotEqual(str(num_dict_4), str(dict_4)) self.assertNotEqual(repr(num_dict_4), repr(dict_4)) self.assertNotIn(repr(num_dict_4), reps) self.assertNotEqual(str(num_dict_5), str(dict_5)) self.assertNotEqual(repr(num_dict_5), repr(dict_5)) self.assertNotIn(repr(num_dict_5), reps) self.assertNotEqual(str(num_dict_6), str(dict_6)) self.assertNotEqual(repr(num_dict_6), repr(dict_6)) self.assertNotIn(repr(num_dict_6), reps) # ...but None keys work just fine self.assertEqual(str(num_dict_7), str(dict_7)) self.assertEqual(repr(num_dict_7), repr(dict_7)) self.assertIn(repr(num_dict_7), reps)