def setUpClass(cls): # Unittests logs will be stored in a `unittests.log` file # in the same directory of the calling unittest. # Use `store_logs=True` in configuration file to activate this cls._logger = CLog( logger_id='unittest', add_stream=True, file_handler='unittests.log' if SECML_STORE_LOGS is True else None) cls._logger.set_level('DEBUG') # As per python 3.2 filterwarnings are reset by unittests for each # TestCase. We thus need to restore our filter for every TestCase global_filterwarnings()
import re from datetime import datetime, timedelta import secml from secml.settings import SECML_LOGS_PATH, SECML_STORE_LOGS from secml.utils import fm, CLog from secml.utils.download_utils import dl_file_gitlab, md5 from secml.settings import SECML_MODELS_DIR MODEL_ZOO_REPO_URL = 'https://gitlab.com/secml/secml-zoo' MODELS_DICT_FILE = 'models_dict.json' MODELS_DICT_PATH = fm.join(SECML_MODELS_DIR, MODELS_DICT_FILE) _logger = CLog( logger_id=__name__, file_handler=SECML_LOGS_PATH if SECML_STORE_LOGS is True else None) def _dl_data_versioned(file_path, output_dir, md5_digest=None): """Download the from different branches depending on version. This function tries to download a model zoo resource from: 1. the branch corresponding to current version, e.g. branch `v0.12` for `0.12.*` version 2. the `master` branch Parameters ---------- file_path : str Path to the file to download, relative to the repository.
def setUpClass(cls): cls.logger = CLog(logger_id=cls.__name__, add_stream=True, level='DEBUG')
class CCreator: """The magnificent global superclass. Attributes ---------- class_type : str Class type identification string. If not defined, class will not be instantiable using `.create()`. __super__ : str or None String with superclass name. Can be None to explicitly NOT support `.create()` and `.load()`. """ __class_type = None # Must be re-defined to support `.create()` __super__ = None # Name of the superclass (if `.create()` or `.load()` should be available) # Ancestor logger, level 'WARNING' by default _logger = CLog( add_stream=True, file_handler=SECML_LOGS_PATH if SECML_STORE_LOGS is True else None) @property def class_type(self): """Defines class type.""" try: # Convert the private attribute to public property return get_private(self.__class__, 'class_type') except AttributeError: raise AttributeError("'class_type' not defined for '{:}'" "".format(self.__class__.__name__)) @property def logger(self): """Logger for current object.""" return self._logger.get_child(self.__class__.__name__ + '.' + str(hex(id(self)))) @property def verbose(self): """Verbosity level of logger output. Available levels are: 0 = no verbose output 1 = info-level logging 2 = debug-level logging """ verbosity_lvls = {30: 0, 20: 1, 10: 2} # 30: WARNING, 20: INFO, 10: DEBUG return verbosity_lvls[self.logger.level] @verbose.setter def verbose(self, level): """Sets verbosity level of logger.""" verbosity_lvls = {0: 'WARNING', 1: 'INFO', 2: 'DEBUG'} if level not in verbosity_lvls: raise ValueError("Verbosity level {:} not supported.".format(level)) self.logger.set_level(verbosity_lvls[level]) @staticmethod def timed(msg=None): """Timer decorator. Returns a decorator that can be used to measure execution time of any method. Performance data will be stored inside the class logger. Messages will be logged using the INFO logging level. As this decorator accepts optional arguments, must be called as a method. See examples. Parameters ---------- msg : str or None, optional Custom message to display when entering the timed block. If None, "Entering timed block `method_name`..." will printed. """ def wrapper(fun): @wraps(fun) # To make wrapped_fun work as fun_timed def wrapped_fun(self, *args, **kwargs): # Wrap fun again and add the timed decorator @self.logger.timed(msg=msg) @wraps(fun) # To make fun_timed work as fun def fun_timed(*fun_args, **fun_wargs): return fun(*fun_args, **fun_wargs) return fun_timed(self, *args, **kwargs) return wrapped_fun return wrapper @classmethod def create(cls, class_item=None, *args, **kwargs): """This method creates an instance of a class with given type. The list of subclasses of calling superclass is looked for any class defining `class_item = 'value'`. If found, the class type is listed. Also a class instance can be passed as main argument. In this case the class instance is returned as is. Parameters ---------- class_item : str or class instance or None, optional Type of the class to instantiate. If a class instance of cls is passed, instead, it returns the instance directly. If this is None, an instance of the classing superclass is created. args, kwargs : optional arguments Any other argument for the class to create. If a class instance is passed as `class_item`, optional arguments are NOT allowed. Returns ------- instance_class : any class Instance of the class having the given type (`class_type`) or the same class instance passed as input. """ if cls.__super__ != cls.__name__: raise TypeError("classes can be created from superclasses only.") # Create an instance of the calling superclass if class_item is None: return cls(*args, **kwargs) # Pycharm says are unexpected args # We accept strings and class instances only if isclass(class_item): # Returns false for instances raise TypeError("creator only accepts a class type " "as string or a class instance.") # CCreator cannot be created! if class_item.__class__ == CCreator: raise TypeError("class 'CCreator' is not callable.") # If a class instance is passed, it's returned as is if not is_str(class_item): if not isinstance(class_item, cls): raise TypeError("input instance should be a {:} " "subclass.".format(cls.__name__)) if len(args) + len(kwargs) != 0: raise TypeError("optional arguments are not allowed " "when a class instance is passed.") return class_item # Get all the subclasses of the superclass subclasses = cls.get_subclasses() # Get all class types from the list of subclasses (to check duplicates) class_types = import_class_types(subclasses) # Check for duplicates _check_class_types_duplicates(class_types, subclasses) # Everything seems fine now, look for desired class type for class_data in subclasses: if get_private(class_data[1], 'class_type', None) == class_item: return class_data[1](*args, **kwargs) raise NameError("no class of type `{:}` is a subclass of '{:}' " "from module '{:}'".format( class_item, cls.__name__, cls.__module__)) @classmethod def get_subclasses(cls): """Get all the subclasses of the calling class. Returns ------- subclasses : list of tuple The list containing a tuple (class.__name__, class) for each subclass of calling class. Keep in mind that in Python each class is a "subclass" of itself. """ def get_subclasses(sup_cls): subcls_list = [] for subclass in sup_cls.__subclasses__(): subcls_list.append((subclass.__name__, subclass)) subcls_list += get_subclasses(subclass) return subcls_list subclasses = get_subclasses(cls) # the superclass is a "subclass" of itself (in Python) subclasses.append((cls.__name__, cls)) return subclasses @classmethod def list_class_types(cls): """This method lists all types of available subclasses of calling one. The list of subclasses of calling superclass is looked for any class defining `class_item = 'value'`. If found, the class type is listed. Returns ------- types : list List of the types of available subclasses of calling class. """ # Why this method is a classmethod? Just an exception for simplicity # classmethods should normally return an instance of calling class if cls.__super__ != cls.__name__: raise TypeError("only superclasses can be used.") # Get all the subclasses of the superclass subclasses = cls.get_subclasses() # Get all class types from the list of subclasses (to check duplicates) class_types = import_class_types(subclasses) # Check for duplicates _check_class_types_duplicates(class_types, subclasses) return class_types @classmethod def get_class_from_type(cls, class_type): """Return the class associated with input type. This will NOT check for classes with duplicated class type. The first class found with matching type will be returned. Parameters ---------- class_type : str Type of the class which will be looked up for. Returns ------- class_obj : class Desired class, if found. This is NOT an instance of the class. """ # Why this method is a classmethod? Just an exception for simplicity # classmethods should normally return an instance of calling class if cls.__super__ != cls.__name__: raise TypeError("only superclasses can be used.") # Get all the subclasses of the superclass subclasses = cls.get_subclasses() # Look for desired class type for class_data in subclasses: if get_private(class_data[1], 'class_type', None) == class_type: return class_data[1] raise NameError("no class of type `{:}` found within the package " "of class '{:}'".format(class_type, cls.__module__)) def get_params(self): """Returns the dictionary of class hyperparameters. A hyperparameter is a PUBLIC or READ/WRITE attribute. """ # We extract the PUBLIC (pub) and the READ/WRITE (rw) attributes # from the class dictionary, than we build a new dictionary using # as keys the attributes names without the accessibility prefix params = SubLevelsDict((as_public(k), getattr(self, as_public(k))) for k in extract_attr(self, 'pub+rw')) # Now look for any parameter inside the accessible attributes for k in extract_attr(self, 'r'): # Extract the contained object (if any) k_attr = getattr(self, as_public(k)) if hasattr(k_attr, 'get_params') and len(k_attr.get_params()) > 0: # as k_attr has one or more parameters, it's a parameter itself params[as_public(k)] = k_attr return params def set_params(self, params_dict, copy=False): """Set all parameters passed as a dictionary {key: value}. This function natively takes as input the dictionary created by `.get_params`. Only parameters, i.e. PUBLIC or READ/WRITE attributes, can be set. For more information on the setting behaviour see `.CCreator.set`. If possible, a reference to the parameter to set is assigned. Use `copy=True` to always make a deepcopy before set. Parameters ---------- params_dict : dict Dictionary of parameters to set. copy : bool By default (False) a reference to the parameter to assign is set. If True or a reference cannot be extracted, a deepcopy of the parameter is done first. See Also -------- get_params : returns the dictionary of class parameters. """ for param_name in params_dict: # Call single attribute set method self.set(param_name, params_dict[param_name], copy) def set(self, param_name, param_value, copy=False): """Set a parameter of the class. Only writable attributes of the class, i.e. PUBLIC or READ/WRITE, can be set. The following checks are performed before setting: - if `param_name` is an attribute of current class, set directly; - else, iterate over __dict__ and look for a class attribute having the desired parameter as an attribute; - else, if attribute is not found on the 2nd level, raise AttributeError. If possible, a reference to the attribute to set is assigned. Use `copy=True` to always make a deepcopy before set. Parameters ---------- param_name : str Name of the parameter to set. param_value : any Value to set for the parameter. copy : bool By default (False) a reference to the parameter to assign is set. If True or a reference cannot be extracted, a deepcopy of the parameter value is done first. """ def copy_attr(attr_tocopy): from copy import deepcopy return deepcopy(attr_tocopy) # Support for recursive setting, e.g. -> kernel.gamma param_name = param_name.split('.') attr0 = param_name[0] if hasattr(self, attr0): # Level 0 set or multiple sublevels set? if len(param_name) == 1: # Set attribute directly # Level 0 attribute must be writable # PUBLIC and READ/WRITE accessibility is checked if not is_writable(self, attr0): raise AttributeError( "can't set `{:}`, must be writable.".format(attr0)) setattr(self, attr0, copy_attr(param_value) if copy is True else param_value) return else: # Start recursion on sublevels # Level 0 attribute must be accessible (readable) # PUBLIC, READ/WRITE and READ ONLY accessibility is checked if not is_readable(self, attr0): raise AttributeError( "can't set `{:}`, must be accessible.".format(attr0)) sub_param_name = '.'.join(param_name[1:]) # Calling `.set` method of the next sublevel getattr(self, attr0).set(sub_param_name, param_value, copy) return # OLD STYLE SET: recursion on 2 levels only to set a subattribute # The first subattribute found is set... else: # Look for the attribute inside all class attributes for attr_name in self.__dict__: # Extract the current attribute attr = getattr(self, attr_name) # If parameter is an attribute of current attribute set it if hasattr(attr, attr0): # Attributes to set must be writable # PUBLIC and READ/WRITE accessibility is checked if not is_writable(attr, attr0): raise AttributeError( "can't set `{:}`, must be writable.".format(attr0)) setattr(attr, attr0, copy_attr(param_value) if copy is True else param_value) return # Attribute not found, raise AttributeError raise AttributeError( "'{:}', or any of its attributes, has attribute '{:}'" "".format(self.__class__.__name__, attr0)) def get_state(self): """Returns the object state dictionary. Returns ------- dict Dictionary containing the state of the object. """ # We extract the PUBLIC (pub), READ/WRITE (rw) and READ ONLY (r) # attributes from the class dictionary, than we build a new dictionary # using as keys the attributes names without the accessibility prefix state = dict((as_public(k), getattr(self, as_public(k))) for k in extract_attr(self, 'pub+rw+r')) # Get the state of the deeper objects # Use list(state) as state size will change during iteration for attr in list(state): if isinstance(state[attr], CCreator): state_deep = state[attr].get_state() # Replace `attr` with its attributes's state for attr_deep in state_deep: attr_full_key = attr + '.' + attr_deep state[attr_full_key] = state_deep[attr_deep] del state[attr] return dict(state) def set_state(self, state_dict, copy=False): """Sets the object state using input dictionary. Only readable attributes of the class, i.e. PUBLIC or READ/WRITE or READ ONLY, can be set. If possible, a reference to the attribute to set is assigned. Use `copy=True` to always make a deepcopy before set. Parameters ---------- state_dict : dict Dictionary containing the state of the object. copy : bool, optional By default (False) a reference to the attribute to assign is set. If True or a reference cannot be extracted, a deepcopy of the attribute is done first. """ def copy_attr(attr_tocopy): from copy import deepcopy return deepcopy(attr_tocopy) for param_name in state_dict: # Extract the value of the attribute to set param_value = state_dict[param_name] # Support for recursive setting, e.g. -> kernel.gamma param_name = param_name.split('.', 1) # Attributes to set in this function must be readable # PUBLIC, READ/WRITE and READ ONLY accessibility is checked if not is_readable(self, param_name[0]): raise AttributeError( "can't set `{:}`, must be readable.".format(param_name[0])) attr0 = param_name[0] if hasattr(self, attr0): # 1 level set or multiple sublevels set? if len(param_name) == 1: # Set attribute directly # If writable (public or property with setter) if is_writable(self, attr0): # Use main `.set` self.set(attr0, param_value, copy=copy) continue # Attribute set, go to next one else: # Maybe is read-only (property with only getter)? # If exists, set the protected attribute if has_protected(self, attr0): attr0 = as_protected(attr0) setattr(self, attr0, copy_attr(param_value) if copy is True else param_value) continue # Attribute set, go to next one else: # Start recursion on sublevels # Call `.set_state` for the next level of current attribute getattr(self, attr0).set_state( {param_name[1]: param_value}, copy) continue # Attribute set, go to next one # Attribute not found, raise AttributeError raise AttributeError( "'{:}', or any of its attributes, has attribute '{:}'" "".format(self.__class__.__name__, attr0)) def copy(self): """Returns a shallow copy of current class. As shallow copy creates a new instance of current object and then insert in the new object a reference (if possible) to each attribute of the original object. """ from copy import copy return copy(self) def __copy__(self, *args, **kwargs): """Called when copy.copy(object) is called.""" from copy import copy new_obj = self.__new__(self.__class__) for attr in self.__dict__: new_obj.__dict__[attr] = copy(self.__dict__[attr]) return new_obj def deepcopy(self): """Returns a deep copy of current class. As deep copy is time consuming in most cases, can sometimes be acceptable to select a subset of attributes and assign them to a new instance of the current class using `.set_params`. """ from copy import deepcopy return deepcopy(self) def __deepcopy__(self, memo, *args, **kwargs): """Called when copy.deepcopy(object) is called. `memo` is a memory dictionary needed by `copy.deepcopy`. """ from copy import deepcopy new_obj = self.__new__(self.__class__) for attr in self.__dict__: new_obj.__dict__[attr] = deepcopy(self.__dict__[attr], memo) return new_obj def save(self, path): """Save class object to file. This function stores an object to file (with pickle). `.load()` can be used to restore the object later. Parameters ---------- path : str Path of the target object file. Returns ------- obj_path : str The full path of the stored object. """ return pck.save(path, self) @classmethod def load(cls, path): """Loads object from file. This function loads an object from file (with pickle). The object can be correctly loaded in the following cases: - loaded and calling class have the same type. - calling class is the superclass of the loaded class's package. - calling class is `.CCreator`. Parameters ---------- path : str Path of the target object file. """ loaded_obj = pck.load(path) if loaded_obj.__class__ == cls or cls == CCreator or \ (has_super(loaded_obj) and cls.__name__ == loaded_obj.__super__): return loaded_obj else: err_str = "'{0}' can be loaded from: '{0}'".format( loaded_obj.__class__.__name__) if has_super(loaded_obj): err_str += ", '{:}'".format(loaded_obj.__super__) raise TypeError(err_str + " or 'CCreator'.") def save_state(self, path): """Store the object state to file. Parameters ---------- path : str Path of the file where to store object state. Returns ------- str The full path of the stored object. See Also -------- get_state : Returns the object state dictionary. """ return pck.save(path, self.get_state()) def load_state(self, path): """Sets the object state from file. Parameters ---------- path : str The full path of the file from which to load the object state. See Also -------- set_state : Sets the object state using input dictionary. """ # Copy not needed for objects loaded from disk self.set_state(pck.load(path), copy=False) def __repr__(self): """Defines print behaviour.""" out_repr = self.__class__.__name__ + "{" for k in extract_attr(self, 'pub+rw+r'): pub_attr_name = as_public(k) out_repr += "'{:}': ".format(pub_attr_name) out_repr += repr(getattr(self, pub_attr_name)) out_repr += ", " return out_repr.rstrip(', ') + "}"