diff --git a/clearml/backend_interface/logger.py b/clearml/backend_interface/logger.py index d3859014..d9a5be2b 100644 --- a/clearml/backend_interface/logger.py +++ b/clearml/backend_interface/logger.py @@ -176,9 +176,11 @@ class PrintPatchLogger(object): patched = False lock = threading.Lock() recursion_protect_lock = threading.RLock() - cr_flush_period = config.get("development.worker.console_cr_flush_period", 0) + cr_flush_period = None def __init__(self, stream, logger=None, level=logging.INFO): + if self.__class__.cr_flush_period is None: + self.__class__.cr_flush_period = config.get("development.worker.console_cr_flush_period", 0) PrintPatchLogger.patched = True self._terminal = stream self._log = logger diff --git a/clearml/backend_interface/metrics/events.py b/clearml/backend_interface/metrics/events.py index b9e173b0..eac07b01 100644 --- a/clearml/backend_interface/metrics/events.py +++ b/clearml/backend_interface/metrics/events.py @@ -12,7 +12,7 @@ from PIL import Image from six.moves.urllib.parse import urlparse, urlunparse from ...backend_api.services import events -from ...config import config +from ...config import config, deferred_config from ...storage.util import quote_url from ...utilities.attrs import attrs from ...utilities.process.mp import SingletonLock @@ -196,14 +196,17 @@ class ImageEventNoUpload(MetricsEventAdapter): class UploadEvent(MetricsEventAdapter): """ Image event adapter """ - _format = '.' + str(config.get('metrics.images.format', 'JPEG')).upper().lstrip('.') - _quality = int(config.get('metrics.images.quality', 87)) - _subsampling = int(config.get('metrics.images.subsampling', 0)) + _format = deferred_config( + 'metrics.images.format', 'JPEG', + transform=lambda x: '.' + str(x).upper().lstrip('.') + ) + _quality = deferred_config('metrics.images.quality', 87, transform=int) + _subsampling = deferred_config('metrics.images.subsampling', 0, transform=int) + _file_history_size = deferred_config('metrics.file_history_size', 5, transform=int) _upload_retries = 3 _metric_counters = {} _metric_counters_lock = SingletonLock() - _file_history_size = int(config.get('metrics.file_history_size', 5)) @staticmethod def _replace_slash(part): diff --git a/clearml/backend_interface/metrics/interface.py b/clearml/backend_interface/metrics/interface.py index 9cf8d1de..c3ab4c7c 100644 --- a/clearml/backend_interface/metrics/interface.py +++ b/clearml/backend_interface/metrics/interface.py @@ -9,7 +9,7 @@ from pathlib2 import Path from ...backend_api.services import events as api_events from ..base import InterfaceBase -from ...config import config +from ...config import config, deferred_config from ...debugging import get_logger from ...storage.helper import StorageHelper @@ -17,18 +17,19 @@ from .events import MetricsEventAdapter from ...utilities.process.mp import SingletonLock -log = get_logger('metrics') - - class Metrics(InterfaceBase): """ Metrics manager and batch writer """ _storage_lock = SingletonLock() - _file_upload_starvation_warning_sec = config.get('network.metrics.file_upload_starvation_warning_sec', None) + _file_upload_starvation_warning_sec = deferred_config('network.metrics.file_upload_starvation_warning_sec', None) _file_upload_retries = 3 _upload_pool = None _file_upload_pool = None __offline_filename = 'metrics.jsonl' + @classmethod + def _get_logger(cls): + return get_logger('metrics') + @property def storage_key_prefix(self): return self._storage_key_prefix @@ -42,7 +43,7 @@ class Metrics(InterfaceBase): storage_uri = storage_uri or self._storage_uri return StorageHelper.get(storage_uri) except Exception as e: - log.error('Failed getting storage helper for %s: %s' % (storage_uri, str(e))) + self._get_logger().error('Failed getting storage helper for %s: %s' % (storage_uri, str(e))) finally: self._storage_lock.release() @@ -153,7 +154,7 @@ class Metrics(InterfaceBase): if e: entries.append(e) except Exception as ex: - log.warning(str(ex)) + self._get_logger().warning(str(ex)) events.remove(ev) # upload the needed files @@ -171,7 +172,7 @@ class Metrics(InterfaceBase): url = storage.upload_from_stream(e.stream, e.url, retries=retries) e.event.update(url=url) except Exception as exp: - log.warning("Failed uploading to {} ({})".format( + self._get_logger().warning("Failed uploading to {} ({})".format( upload_uri if upload_uri else "(Could not calculate upload uri)", exp, )) @@ -196,15 +197,16 @@ class Metrics(InterfaceBase): t_f, t_u, t_ref = \ (self._file_related_event_time, self._file_upload_time, self._file_upload_starvation_warning_sec) if t_f and t_u and t_ref and (t_f - t_u) > t_ref: - log.warning('Possible metrics file upload starvation: ' - 'files were not uploaded for {} seconds'.format(t_ref)) + self._get_logger().warning( + 'Possible metrics file upload starvation: ' + 'files were not uploaded for {} seconds'.format(t_ref)) # send the events in a batched request good_events = [ev for ev in events if ev.upload_exception is None] error_events = [ev for ev in events if ev.upload_exception is not None] if error_events: - log.error("Not uploading {}/{} events because the data upload failed".format( + self._get_logger().error("Not uploading {}/{} events because the data upload failed".format( len(error_events), len(events), )) diff --git a/clearml/backend_interface/metrics/reporter.py b/clearml/backend_interface/metrics/reporter.py index da61baf9..ca30405f 100644 --- a/clearml/backend_interface/metrics/reporter.py +++ b/clearml/backend_interface/metrics/reporter.py @@ -167,6 +167,9 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan """ log = metrics.log.getChild('reporter') log.setLevel(log.level) + if self.__class__.max_float_num_digits is -1: + self.__class__.max_float_num_digits = config.get('metrics.plot_max_num_digits', None) + super(Reporter, self).__init__(session=metrics.session, log=log) self._metrics = metrics self._bucket_config = None @@ -185,7 +188,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan self._report_service.set_storage_uri(self._storage_uri) storage_uri = property(None, _set_storage_uri) - max_float_num_digits = config.get('metrics.plot_max_num_digits', None) + max_float_num_digits = -1 @property def async_enable(self): diff --git a/clearml/backend_interface/task/development/stop_signal.py b/clearml/backend_interface/task/development/stop_signal.py index 3b996c68..066bff68 100644 --- a/clearml/backend_interface/task/development/stop_signal.py +++ b/clearml/backend_interface/task/development/stop_signal.py @@ -1,4 +1,4 @@ -from ....config import config +from ....config import config, deferred_config class TaskStopReason(object): @@ -8,7 +8,7 @@ class TaskStopReason(object): class TaskStopSignal(object): - enabled = bool(config.get('development.support_stopping', False)) + enabled = deferred_config('development.support_stopping', False, transform=bool) _number_of_consecutive_reset_tests = 4 diff --git a/clearml/backend_interface/task/development/worker.py b/clearml/backend_interface/task/development/worker.py index a8947278..776f9f38 100644 --- a/clearml/backend_interface/task/development/worker.py +++ b/clearml/backend_interface/task/development/worker.py @@ -3,7 +3,7 @@ from threading import Thread, Event from time import time -from ....config import config +from ....config import config, deferred_config from ....backend_interface.task.development.stop_signal import TaskStopSignal from ....backend_api.services import tasks @@ -11,9 +11,13 @@ from ....backend_api.services import tasks class DevWorker(object): prefix = attr.ib(type=str, default="MANUAL:") - report_period = float(max(config.get('development.worker.report_period_sec', 30.), 1.)) - report_stdout = bool(config.get('development.worker.log_stdout', True)) - ping_period = float(max(config.get('development.worker.ping_period_sec', 30.), 1.)) + report_stdout = deferred_config('development.worker.log_stdout', True) + report_period = deferred_config( + 'development.worker.report_period_sec', 30., + transform=lambda x: float(max(x, 1.0))) + ping_period = deferred_config( + 'development.worker.ping_period_sec', 30., + transform=lambda x: float(max(x, 1.0))) def __init__(self): self._dev_stop_signal = None diff --git a/clearml/backend_interface/task/repo/detectors.py b/clearml/backend_interface/task/repo/detectors.py index 48692571..5a493594 100644 --- a/clearml/backend_interface/task/repo/detectors.py +++ b/clearml/backend_interface/task/repo/detectors.py @@ -18,8 +18,6 @@ from ....config.defs import ( from ....debugging import get_logger from .util import get_command_output -_logger = get_logger("Repository Detection") - class DetectionError(Exception): pass @@ -53,6 +51,10 @@ class Detector(object): _fallback = '_fallback' _remote = '_remote' + @classmethod + def _get_logger(cls): + return get_logger("Repository Detection") + @attr.s class Commands(object): """" Repository information as queried by a detector """ @@ -93,9 +95,9 @@ class Detector(object): return get_command_output(fallback_command, path, strip=strip) except (CalledProcessError, UnicodeDecodeError): pass - _logger.warning("Can't get {} information for {} repo in {}".format(name, self.type_name, path)) + self._get_logger().warning("Can't get {} information for {} repo in {}".format(name, self.type_name, path)) # full details only in debug - _logger.debug( + self._get_logger().debug( "Can't get {} information for {} repo in {}: {}".format( name, self.type_name, path, str(ex) ) @@ -180,7 +182,7 @@ class Detector(object): == 0 ) except CalledProcessError: - _logger.warning("Can't get {} status".format(self.type_name)) + self._get_logger().warning("Can't get {} status".format(self.type_name)) except (OSError, EnvironmentError, IOError): # File not found or can't be executed pass diff --git a/clearml/backend_interface/task/repo/scriptinfo.py b/clearml/backend_interface/task/repo/scriptinfo.py index 68a0d47f..cc2dcef5 100644 --- a/clearml/backend_interface/task/repo/scriptinfo.py +++ b/clearml/backend_interface/task/repo/scriptinfo.py @@ -13,26 +13,30 @@ from threading import Thread, Event from .util import get_command_output, remove_user_pass_from_url from ....backend_api import Session -from ....config import config +from ....config import config, deferred_config from ....debugging import get_logger from .detectors import GitEnvDetector, GitDetector, HgEnvDetector, HgDetector, Result as DetectionResult -_logger = get_logger("Repository Detection") - class ScriptInfoError(Exception): pass class ScriptRequirements(object): + _detailed_import_report = deferred_config('development.detailed_import_report', False) _max_requirements_size = 512 * 1024 _packages_remove_version = ('setuptools', ) _ignore_packages = set() + @classmethod + def _get_logger(cls): + return get_logger("Repository Detection") + def __init__(self, root_folder): self._root_folder = root_folder - def get_requirements(self, entry_point_filename=None, add_missing_installed_packages=False): + def get_requirements(self, entry_point_filename=None, add_missing_installed_packages=False, + detailed_req_report=None): # noinspection PyBroadException try: from ....utilities.pigar.reqs import get_installed_pkgs_detail @@ -48,9 +52,9 @@ class ScriptRequirements(object): for k in guess: if k not in reqs: reqs[k] = guess[k] - return self.create_requirements_txt(reqs, local_pks) + return self.create_requirements_txt(reqs, local_pks, detailed=detailed_req_report) except Exception as ex: - _logger.warning("Failed auto-generating package requirements: {}".format(ex)) + self._get_logger().warning("Failed auto-generating package requirements: {}".format(ex)) return '', '' @staticmethod @@ -115,8 +119,11 @@ class ScriptRequirements(object): return modules @staticmethod - def create_requirements_txt(reqs, local_pks=None): + def create_requirements_txt(reqs, local_pks=None, detailed=None): # write requirements.txt + if detailed is None: + detailed = ScriptRequirements._detailed_import_report + # noinspection PyBroadException try: conda_requirements = '' @@ -193,30 +200,31 @@ class ScriptRequirements(object): for k in sorted(forced_packages.keys()): requirements_txt += ScriptRequirements._make_req_line(k, forced_packages.get(k)) - requirements_txt_packages_only = \ - requirements_txt + '\n# Skipping detailed import analysis, it is too large\n' + if detailed: + requirements_txt_packages_only = \ + requirements_txt + '\n# Skipping detailed import analysis, it is too large\n' - # requirements details (in comments) - requirements_txt += '\n' + \ - '# Detailed import analysis\n' \ - '# **************************\n' + # requirements details (in comments) + requirements_txt += '\n' + \ + '# Detailed import analysis\n' \ + '# **************************\n' - if local_pks: - for k, v in local_pks.sorted_items(): + if local_pks: + for k, v in local_pks.sorted_items(): + requirements_txt += '\n' + requirements_txt += '# IMPORT LOCAL PACKAGE {0}\n'.format(k) + requirements_txt += ''.join(['# {0}\n'.format(c) for c in v.comments.sorted_items()]) + + for k, v in reqs.sorted_items(): + if not v: + continue requirements_txt += '\n' - requirements_txt += '# IMPORT LOCAL PACKAGE {0}\n'.format(k) + if k == '-e': + requirements_txt += '# IMPORT PACKAGE {0} {1}\n'.format(k, v.version) + else: + requirements_txt += '# IMPORT PACKAGE {0}\n'.format(k) requirements_txt += ''.join(['# {0}\n'.format(c) for c in v.comments.sorted_items()]) - for k, v in reqs.sorted_items(): - if not v: - continue - requirements_txt += '\n' - if k == '-e': - requirements_txt += '# IMPORT PACKAGE {0} {1}\n'.format(k, v.version) - else: - requirements_txt += '# IMPORT PACKAGE {0}\n'.format(k) - requirements_txt += ''.join(['# {0}\n'.format(c) for c in v.comments.sorted_items()]) - # make sure we do not exceed the size a size limit return (requirements_txt if len(requirements_txt) < ScriptRequirements._max_requirements_size else requirements_txt_packages_only, @@ -252,7 +260,11 @@ class _JupyterObserver(object): _sample_frequency = 30. _first_sample_frequency = 3. _jupyter_history_logger = None - _store_notebook_artifact = config.get('development.store_jupyter_notebook_artifact', True) + _store_notebook_artifact = deferred_config('development.store_jupyter_notebook_artifact', True) + + @classmethod + def _get_logger(cls): + return get_logger("Repository Detection") @classmethod def observer(cls, jupyter_notebook_filename, log_history): @@ -296,7 +308,7 @@ class _JupyterObserver(object): from nbconvert.exporters.script import ScriptExporter _script_exporter = ScriptExporter() except Exception as ex: - _logger.warning('Could not read Jupyter Notebook: {}'.format(ex)) + cls._get_logger().warning('Could not read Jupyter Notebook: {}'.format(ex)) return # load pigar # noinspection PyBroadException @@ -486,6 +498,10 @@ class ScriptInfo(object): plugins = [GitEnvDetector(), HgEnvDetector(), HgDetector(), GitDetector()] """ Script info detection plugins, in order of priority """ + @classmethod + def _get_logger(cls): + return get_logger("Repository Detection") + @classmethod def _jupyter_install_post_store_hook(cls, jupyter_notebook_filename, log_history=False): # noinspection PyBroadException @@ -542,7 +558,7 @@ class ScriptInfo(object): from ....config import config password = config.get('development.jupyter_server_password', '') if not password: - _logger.warning( + cls._get_logger().warning( 'Password protected Jupyter Notebook server was found! ' 'Add `sdk.development.jupyter_server_password=` to ~/clearml.conf') return os.path.join(os.getcwd(), 'error_notebook_not_found.py') @@ -575,7 +591,7 @@ class ScriptInfo(object): try: r.raise_for_status() except Exception as ex: - _logger.warning('Failed accessing the jupyter server{}: {}'.format( + cls._get_logger().warning('Failed accessing the jupyter server{}: {}'.format( ' [password={}]'.format(password) if server_info.get('password') else '', ex)) return os.path.join(os.getcwd(), 'error_notebook_not_found.py') @@ -630,7 +646,7 @@ class ScriptInfo(object): if entry_point_alternative.exists(): entry_point = entry_point_alternative except Exception as ex: - _logger.warning('Failed accessing jupyter notebook {}: {}'.format(notebook_path, ex)) + cls._get_logger().warning('Failed accessing jupyter notebook {}: {}'.format(notebook_path, ex)) # get local ipynb for observer local_ipynb_file = entry_point.as_posix() @@ -714,16 +730,18 @@ class ScriptInfo(object): @classmethod def _get_script_info( cls, filepaths, check_uncommitted=True, create_requirements=True, log=None, - uncommitted_from_remote=False, detect_jupyter_notebook=True, add_missing_installed_packages=False): + uncommitted_from_remote=False, detect_jupyter_notebook=True, + add_missing_installed_packages=False, detailed_req_report=None): jupyter_filepath = cls._get_jupyter_notebook_filename() if detect_jupyter_notebook else None if jupyter_filepath: scripts_path = [Path(os.path.normpath(jupyter_filepath)).absolute()] else: cwd = cls._cwd() scripts_path = [Path(cls._absolute_path(os.path.normpath(f), cwd)) for f in filepaths if f] - if all(not f.is_file() for f in scripts_path): + scripts_path = [f for f in scripts_path if f.exists()] + if not scripts_path: raise ScriptInfoError( - "Script file {} could not be found".format(scripts_path) + "Script file {} could not be found".format(filepaths) ) scripts_dir = [f.parent for f in scripts_path] @@ -737,10 +755,11 @@ class ScriptInfo(object): ) ) - plugin = next((p for p in cls.plugins if any(p.exists(d) for d in scripts_dir)), None) - repo_info = DetectionResult() script_dir = scripts_dir[0] script_path = scripts_path[0] + plugin = next((p for p in cls.plugins if p.exists(script_dir)), None) + + repo_info = DetectionResult() messages = [] auxiliary_git_diff = None @@ -749,13 +768,8 @@ class ScriptInfo(object): log.info("No repository found, storing script code instead") else: try: - for i, d in enumerate(scripts_dir): - repo_info = plugin.get_info( - str(d), include_diff=check_uncommitted, diff_from_remote=uncommitted_from_remote) - if not repo_info.is_empty(): - script_dir = d - script_path = scripts_path[i] - break + repo_info = plugin.get_info( + str(script_dir), include_diff=check_uncommitted, diff_from_remote=uncommitted_from_remote) except SystemExit: raise except Exception as ex: @@ -799,7 +813,9 @@ class ScriptInfo(object): requirements, conda_requirements = script_requirements.get_requirements( entry_point_filename=script_path.as_posix() if not repo_info.url and script_path.is_file() else None, - add_missing_installed_packages=add_missing_installed_packages) + add_missing_installed_packages=add_missing_installed_packages, + detailed_req_report=detailed_req_report, + ) else: script_requirements = None @@ -831,7 +847,8 @@ class ScriptInfo(object): @classmethod def get(cls, filepaths=None, check_uncommitted=True, create_requirements=True, log=None, - uncommitted_from_remote=False, detect_jupyter_notebook=True, add_missing_installed_packages=False): + uncommitted_from_remote=False, detect_jupyter_notebook=True, add_missing_installed_packages=False, + detailed_req_report=None): try: if not filepaths: filepaths = [sys.argv[0], ] @@ -842,6 +859,7 @@ class ScriptInfo(object): uncommitted_from_remote=uncommitted_from_remote, detect_jupyter_notebook=detect_jupyter_notebook, add_missing_installed_packages=add_missing_installed_packages, + detailed_req_report=detailed_req_report, ) except SystemExit: pass diff --git a/clearml/backend_interface/task/task.py b/clearml/backend_interface/task/task.py index 1b6c6952..7016eb40 100644 --- a/clearml/backend_interface/task/task.py +++ b/clearml/backend_interface/task/task.py @@ -47,7 +47,7 @@ from ..util import ( exact_match_regex, mutually_exclusive, ) from ...config import ( get_config_for_bucket, get_remote_task_id, TASK_ID_ENV_VAR, - running_remotely, get_cache_dir, DOCKER_IMAGE_ENV_VAR, get_offline_dir, get_log_to_backend, ) + running_remotely, get_cache_dir, DOCKER_IMAGE_ENV_VAR, get_offline_dir, get_log_to_backend, deferred_config, ) from ...debugging import get_logger from ...storage.helper import StorageHelper, StorageError from .access import AccessMixin @@ -70,12 +70,11 @@ class Task(IdObjectBase, AccessMixin, SetupUploadMixin): _force_requirements = {} _ignore_requirements = set() - _store_diff = config.get('development.store_uncommitted_code_diff', False) - _store_remote_diff = config.get('development.store_code_diff_from_remote', False) - _report_subprocess_enabled = config.get('development.report_use_subprocess', sys.platform == 'linux') - _force_use_pip_freeze = \ - config.get('development.detect_with_pip_freeze', False) or \ - config.get('development.detect_with_conda_freeze', False) + _store_diff = deferred_config('development.store_uncommitted_code_diff', False) + _store_remote_diff = deferred_config('development.store_code_diff_from_remote', False) + _report_subprocess_enabled = deferred_config('development.report_use_subprocess', sys.platform == 'linux') + _force_use_pip_freeze = deferred_config(multi=[('development.detect_with_pip_freeze', False), + ('development.detect_with_conda_freeze', False)]) _offline_filename = 'task.json' class TaskTypes(Enum): @@ -1815,6 +1814,12 @@ class Task(IdObjectBase, AccessMixin, SetupUploadMixin): return True + def _get_runtime_properties(self): + # type: () -> Mapping[str, str] + if not Session.check_min_api_version('2.13'): + return dict() + return dict(**self.data.runtime) if self.data.runtime else dict() + def _clear_task(self, system_tags=None, comment=None): # type: (Optional[Sequence[str]], Optional[str]) -> () self._data.script = tasks.Script( diff --git a/clearml/config/__init__.py b/clearml/config/__init__.py index a55e30ea..69316941 100644 --- a/clearml/config/__init__.py +++ b/clearml/config/__init__.py @@ -5,14 +5,70 @@ import sys from os.path import expandvars, expanduser from ..backend_api import load_config +from ..backend_config import Config from ..backend_config.bucket_config import S3BucketConfigurations from .defs import * # noqa: F403 from .remote import running_remotely_task_id as _running_remotely_task_id -config_obj = load_config(Path(__file__).parent) # noqa: F405 -config_obj.initialize_logging() -config = config_obj.get("sdk") +# config_obj = load_config(Path(__file__).parent) # noqa: F405 +# config_obj.initialize_logging() +# config = config_obj.get("sdk") +from ..utilities.proxy_object import LazyEvalWrapper + + +class ConfigWrapper(object): + _config = None + + @classmethod + def _init(cls): + if cls._config is None: + cls._config = load_config(Path(__file__).parent) # noqa: F405 + cls._config.initialize_logging() + + @classmethod + def get(cls, *args, **kwargs): + cls._init() + return cls._config.get(*args, **kwargs) + + @classmethod + def set_overrides(cls, *args, **kwargs): + cls._init() + return cls._config.set_overrides(*args, **kwargs) + + +class ConfigSDKWrapper(object): + _config_sdk = None + + @classmethod + def _init(cls): + if cls._config_sdk is None: + cls._config_sdk = ConfigWrapper.get("sdk") + + @classmethod + def get(cls, *args, **kwargs): + cls._init() + return cls._config_sdk.get(*args, **kwargs) + + @classmethod + def set_overrides(cls, *args, **kwargs): + cls._init() + return cls._config_sdk.set_overrides(*args, **kwargs) + + +def deferred_config(key=None, default=Config._MISSING, transform=None, multi=None): + return LazyEvalWrapper( + callback=lambda: + (ConfigSDKWrapper.get(key, default) if not multi else + next(ConfigSDKWrapper.get(*a) for a in multi if ConfigSDKWrapper.get(*a))) + if transform is None + else (transform() if key is None else transform(ConfigSDKWrapper.get(key, default) if not multi else # noqa + next(ConfigSDKWrapper.get(*a) for a in multi if ConfigSDKWrapper.get(*a))))) + + +config_obj = ConfigWrapper +config = ConfigSDKWrapper + """ Configuration object reflecting the merged SDK section of all available configuration files """ diff --git a/clearml/logger.py b/clearml/logger.py index 6d60db33..b0122d78 100644 --- a/clearml/logger.py +++ b/clearml/logger.py @@ -19,7 +19,7 @@ from .backend_interface.logger import StdStreamPatch from .backend_interface.task import Task as _Task from .backend_interface.task.log import TaskHandler from .backend_interface.util import mutually_exclusive -from .config import running_remotely, get_cache_dir, config, DEBUG_SIMULATE_REMOTE_TASK +from .config import running_remotely, get_cache_dir, config, DEBUG_SIMULATE_REMOTE_TASK, deferred_config from .errors import UsageError from .storage.helper import StorageHelper from .utilities.plotly_reporter import SeriesInfo @@ -57,7 +57,7 @@ class Logger(object): """ SeriesInfo = SeriesInfo _tensorboard_logging_auto_group_scalars = False - _tensorboard_single_series_per_graph = config.get('metrics.tensorboard_single_series_per_graph', False) + _tensorboard_single_series_per_graph = deferred_config('metrics.tensorboard_single_series_per_graph', False) def __init__(self, private_task, connect_stdout=True, connect_stderr=True, connect_logging=False): """ @@ -192,19 +192,18 @@ class Logger(object): You can view the vectors plots in the **ClearML Web-App (UI)**, **RESULTS** tab, **PLOTS** sub-tab. - :param str title: The title (metric) of the plot. - :param str series: The series name (variant) of the reported histogram. - :param list(float) values: The series values. A list of floats, or an N-dimensional Numpy array containing + :param title: The title (metric) of the plot. + :param series: The series name (variant) of the reported histogram. + :param values: The series values. A list of floats, or an N-dimensional Numpy array containing data for each histogram bar. - :type values: list(float), numpy.ndarray - :param int iteration: The reported iteration / step. Each ``iteration`` creates another plot. - :param list(str) labels: Labels for each bar group, creating a plot legend labeling each series. (Optional) - :param list(str) xlabels: Labels per entry in each bucket in the histogram (vector), creating a set of labels + :param iteration: The reported iteration / step. Each ``iteration`` creates another plot. + :param labels: Labels for each bar group, creating a plot legend labeling each series. (Optional) + :param xlabels: Labels per entry in each bucket in the histogram (vector), creating a set of labels for each histogram bar on the x-axis. (Optional) - :param str xaxis: The x-axis title. (Optional) - :param str yaxis: The y-axis title. (Optional) - :param str mode: Multiple histograms mode, stack / group / relative. Default is 'group'. - :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + :param xaxis: The x-axis title. (Optional) + :param yaxis: The y-axis title. (Optional) + :param mode: Multiple histograms mode, stack / group / relative. Default is 'group'. + :param extra_layout: optional dictionary for layout configuration, passed directly to plotly example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ self._touch_title_series(title, series) @@ -239,19 +238,18 @@ class Logger(object): You can view the reported histograms in the **ClearML Web-App (UI)**, **RESULTS** tab, **PLOTS** sub-tab. - :param str title: The title (metric) of the plot. - :param str series: The series name (variant) of the reported histogram. - :param list(float) values: The series values. A list of floats, or an N-dimensional Numpy array containing + :param title: The title (metric) of the plot. + :param series: The series name (variant) of the reported histogram. + :param values: The series values. A list of floats, or an N-dimensional Numpy array containing data for each histogram bar. - :type values: list(float), numpy.ndarray - :param int iteration: The reported iteration / step. Each ``iteration`` creates another plot. - :param list(str) labels: Labels for each bar group, creating a plot legend labeling each series. (Optional) - :param list(str) xlabels: Labels per entry in each bucket in the histogram (vector), creating a set of labels + :param iteration: The reported iteration / step. Each ``iteration`` creates another plot. + :param labels: Labels for each bar group, creating a plot legend labeling each series. (Optional) + :param xlabels: Labels per entry in each bucket in the histogram (vector), creating a set of labels for each histogram bar on the x-axis. (Optional) - :param str xaxis: The x-axis title. (Optional) - :param str yaxis: The y-axis title. (Optional) - :param str mode: Multiple histograms mode, stack / group / relative. Default is 'group'. - :param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly + :param xaxis: The x-axis title. (Optional) + :param yaxis: The y-axis title. (Optional) + :param mode: Multiple histograms mode, stack / group / relative. Default is 'group'. + :param extra_layout: optional dictionary for layout configuration, passed directly to plotly example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} """ @@ -307,18 +305,14 @@ class Logger(object): You can view the reported tables in the **ClearML Web-App (UI)**, **RESULTS** tab, **PLOTS** sub-tab. - :param str title: The title (metric) of the table. - :param str series: The series name (variant) of the reported table. - :param int iteration: The reported iteration / step. + :param title: The title (metric) of the table. + :param series: The series name (variant) of the reported table. + :param iteration: The reported iteration / step. :param table_plot: The output table plot object - :type table_plot: pandas.DataFrame or Table as list of rows (list) :param csv: path to local csv file - :type csv: str :param url: A URL to the location of csv file. - :type url: str :param extra_layout: optional dictionary for layout configuration, passed directly to plotly example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}} - :type extra_layout: dict """ mutually_exclusive( UsageError, _check_none=True, @@ -769,7 +763,7 @@ class Logger(object): """ For explicit reporting, report an image and upload its contents. - This method uploads the image to a preconfigured bucket (see :meth:`Logger.setup_upload`) with a key (filename) + This method uploads the image to a preconfigured bucket (see :meth:`Logger.set_default_upload_destination`) describing the task ID, title, series and iteration. For example: @@ -790,22 +784,20 @@ class Logger(object): - ``image`` - ``matrix`` - :param str title: The title (metric) of the image. - :param str series: The series name (variant) of the reported image. - :param int iteration: The reported iteration / step. - :param str local_path: A path to an image file. - :param str url: A URL for the location of a pre-uploaded image. + :param title: The title (metric) of the image. + :param series: The series name (variant) of the reported image. + :param iteration: The reported iteration / step. + :param local_path: A path to an image file. + :param url: A URL for the location of a pre-uploaded image. :param image: Image data (RGB). - :type image: numpy.ndarray, PIL.Image.Image - :param numpy.ndarray matrix: Image data (RGB). + :param matrix: Deperacted, Image data (RGB). .. note:: - The ``matrix`` paramater is deprecated. Use the ``image`` parameters. - :type matrix: 3D numpy.ndarray - :param int max_image_history: The maximum number of images to store per metric/variant combination. + The ``matrix`` parameter is deprecated. Use the ``image`` parameters. + :param max_image_history: The maximum number of images to store per metric/variant combination. For an unlimited number, use a negative value. The default value is set in global configuration (default=``5``). - :param bool delete_after_upload: After the upload, delete the local copy of the image + :param delete_after_upload: After the upload, delete the local copy of the image The values are: @@ -1147,7 +1139,6 @@ class Logger(object): - ``True`` - Scalars without specific titles are grouped together in the "Scalars" plot, preserving backward compatibility with ClearML automagical behavior. - ``False`` - TensorBoard scalars without titles get a title/series with the same tag. (default) - :type group_scalars: bool """ cls._tensorboard_logging_auto_group_scalars = group_scalars @@ -1165,7 +1156,6 @@ class Logger(object): - ``True`` - Generate a separate plot for each TensorBoard scalar series. - ``False`` - Group the TensorBoard scalar series together in the same plot. (default) - :type single_series: bool """ cls._tensorboard_single_series_per_graph = single_series @@ -1205,11 +1195,10 @@ class Logger(object): """ print text to log (same as print to console, and also prints to console) - :param str msg: text to print to the console (always send to the backend and displayed in console) + :param msg: text to print to the console (always send to the backend and displayed in console) :param level: logging level, default: logging.INFO - :type level: Logging Level - :param bool omit_console: Omit the console output, and only send the ``msg`` value to the log - :param bool force_send: Report with an explicit log level. Only supported if ``omit_console`` is True + :param omit_console: Omit the console output, and only send the ``msg`` value to the log + :param force_send: Report with an explicit log level. Only supported if ``omit_console`` is True - ``True`` - Omit the console output. - ``False`` - Print the console output. (default) @@ -1271,24 +1260,17 @@ class Logger(object): """ Report an image, upload its contents, and present in plots section using plotly - Image is uploaded to a preconfigured bucket (see :meth:`Logger.setup_upload`) with a key (filename) + Image is uploaded to a preconfigured bucket (see :meth:`Logger.set_default_upload_destination`) describing the task ID, title, series and iteration. :param title: Title (AKA metric) - :type title: str :param series: Series (AKA variant) - :type series: str :param iteration: Iteration number - :type iteration: int :param path: A path to an image file. Required unless matrix is provided. - :type path: str :param matrix: A 3D numpy.ndarray object containing image data (RGB). Required unless filename is provided. - :type matrix: np.array :param max_image_history: maximum number of image to store per metric/variant combination \ use negative value for unlimited. default is set in global configuration (default=5) - :type max_image_history: int :param delete_after_upload: if True, one the file was uploaded the local copy will be deleted - :type delete_after_upload: boolean """ # if task was not started, we have to start it @@ -1326,22 +1308,16 @@ class Logger(object): """ Upload a file and report it as link in the debug images section. - File is uploaded to a preconfigured storage (see :meth:`Logger.setup_upload`) with a key (filename) + File is uploaded to a preconfigured storage (see :meth:`Loggerset_default_upload_destination`) describing the task ID, title, series and iteration. :param title: Title (AKA metric) - :type title: str :param series: Series (AKA variant) - :type series: str :param iteration: Iteration number - :type iteration: int :param path: A path to file to be uploaded - :type path: str :param max_file_history: maximum number of files to store per metric/variant combination \ use negative value for unlimited. default is set in global configuration (default=5) - :type max_file_history: int :param delete_after_upload: if True, one the file was uploaded the local copy will be deleted - :type delete_after_upload: boolean """ # if task was not started, we have to start it diff --git a/clearml/storage/cache.py b/clearml/storage/cache.py index feb231db..6a76047e 100644 --- a/clearml/storage/cache.py +++ b/clearml/storage/cache.py @@ -6,13 +6,13 @@ from pathlib2 import Path from .helper import StorageHelper from .util import quote_url -from ..config import get_cache_dir, config +from ..config import get_cache_dir, deferred_config from ..debugging.log import LoggerRoot class CacheManager(object): __cache_managers = {} - _default_cache_file_limit = config.get("storage.cache.default_cache_manager_size", 100) + _default_cache_file_limit = deferred_config("storage.cache.default_cache_manager_size", 100) _storage_manager_folder = "storage_manager" _default_context = "global" _local_to_remote_url_lookup = OrderedDict() @@ -155,7 +155,8 @@ class CacheManager(object): cache_context = cache_context or cls._default_context if cache_context not in cls.__cache_managers: cls.__cache_managers[cache_context] = cls.CacheContext( - cache_context, cache_file_limit or cls._default_cache_file_limit + cache_context, + cache_file_limit or cls._default_cache_file_limit ) if cache_file_limit: cls.__cache_managers[cache_context].set_cache_limit(cache_file_limit) diff --git a/clearml/storage/helper.py b/clearml/storage/helper.py index 3c7a8e32..e296ccc4 100644 --- a/clearml/storage/helper.py +++ b/clearml/storage/helper.py @@ -34,19 +34,10 @@ from .callbacks import UploadProgressReport, DownloadProgressReport from .util import quote_url from ..backend_api.utils import get_http_session_with_retry from ..backend_config.bucket_config import S3BucketConfigurations, GSBucketConfigurations, AzureContainerConfigurations -from ..config import config +from ..config import config, deferred_config from ..debugging import get_logger from ..errors import UsageError -log = get_logger('storage') -level = config.get('storage.log.level', None) - -if level: - try: - log.setLevel(level) - except (TypeError, ValueError): - log.error('invalid storage log level in configuration: %s' % level) - class StorageError(Exception): pass @@ -59,6 +50,10 @@ class DownloadError(Exception): @six.add_metaclass(ABCMeta) class _Driver(object): + @classmethod + def get_logger(cls): + return get_logger('storage') + @abstractmethod def get_container(self, container_name, config=None, **kwargs): pass @@ -107,6 +102,10 @@ class StorageHelper(object): """ _temp_download_suffix = '.partially' + @classmethod + def _get_logger(cls): + return get_logger('storage') + @attrs class _PathSubstitutionRule(object): registered_prefix = attrib(type=str) @@ -128,7 +127,7 @@ class StorageHelper(object): ) if any(prefix is None for prefix in (rule.registered_prefix, rule.local_prefix)): - log.warning( + StorageHelper._get_logger().warning( "Illegal substitution rule configuration '{}[{}]': {}".format( cls.path_substitution_config, index, @@ -138,7 +137,7 @@ class StorageHelper(object): continue if all((rule.replace_windows_sep, rule.replace_linux_sep)): - log.warning( + StorageHelper._get_logger().warning( "Only one of replace_windows_sep and replace_linux_sep flags may be set." "'{}[{}]': {}".format( cls.path_substitution_config, @@ -190,11 +189,10 @@ class StorageHelper(object): _upload_pool = None # collect all bucket credentials that aren't empty (ignore entries with an empty key or secret) - _s3_configurations = S3BucketConfigurations.from_config(config.get('aws.s3', {})) - _gs_configurations = GSBucketConfigurations.from_config(config.get('google.storage', {})) - _azure_configurations = AzureContainerConfigurations.from_config(config.get('azure.storage', {})) - - _path_substitutions = _PathSubstitutionRule.load_list_from_config() + _s3_configurations = deferred_config('aws.s3', {}, transform=S3BucketConfigurations.from_config) + _gs_configurations = deferred_config('google.storage', {}, transform=GSBucketConfigurations.from_config) + _azure_configurations = deferred_config('azure.storage', {}, transform=AzureContainerConfigurations.from_config) + _path_substitutions = deferred_config(transform=_PathSubstitutionRule.load_list_from_config) @property def log(self): @@ -236,10 +234,10 @@ class StorageHelper(object): try: instance = cls(base_url=base_url, url=url, logger=logger, canonize_url=False, **kwargs) except (StorageError, UsageError) as ex: - log.error(str(ex)) + cls._get_logger().error(str(ex)) return None except Exception as ex: - log.error("Failed creating storage object {} Reason: {}".format( + cls._get_logger().error("Failed creating storage object {} Reason: {}".format( base_url or url, ex)) return None @@ -264,7 +262,15 @@ class StorageHelper(object): def __init__(self, base_url, url, key=None, secret=None, region=None, verbose=False, logger=None, retries=5, **kwargs): - self._log = logger or log + level = config.get('storage.log.level', None) + + if level: + try: + self._get_logger().setLevel(level) + except (TypeError, ValueError): + self._get_logger().error('invalid storage log level in configuration: %s' % level) + + self._log = logger or self._get_logger() self._verbose = verbose self._retries = retries self._extra = {} @@ -856,7 +862,7 @@ class StorageHelper(object): else: folder_uri = '/'.join((_base_url, folder_uri)) - log.debug('Upload destination {} amended to {} for registration purposes'.format( + cls._get_logger().debug('Upload destination {} amended to {} for registration purposes'.format( prev_folder_uri, folder_uri)) else: raise ValueError('folder_uri: {} does not start with base url: {}'.format(folder_uri, _base_url)) @@ -1055,7 +1061,8 @@ class _HttpDriver(_Driver): container = self._containers[obj.container_name] res = container.session.delete(obj.url, headers=container.get_headers(obj.url)) if res.status_code != requests.codes.ok: - log.warning('Failed deleting object %s (%d): %s' % (obj.object_name, res.status_code, res.text)) + self._get_logger().warning('Failed deleting object %s (%d): %s' % ( + obj.object_name, res.status_code, res.text)) return False return True @@ -1086,7 +1093,7 @@ class _HttpDriver(_Driver): obj = self._get_download_object(obj) p = Path(local_path) if not overwrite_existing and p.is_file(): - log.warning('failed saving after download: overwrite=False and file exists (%s)' % str(p)) + self.get_logger().warning('failed saving after download: overwrite=False and file exists (%s)' % str(p)) return length = 0 with p.open(mode='wb') as f: @@ -1156,7 +1163,7 @@ class _Stream(object): self.closed = True raise StopIteration() except Exception as ex: - log.error('Failed downloading: %s' % ex) + _Driver.get_logger().error('Failed downloading: %s' % ex) else: # in/out stream try: @@ -1210,10 +1217,9 @@ class _Stream(object): class _Boto3Driver(_Driver): """ Boto3 storage adapter (simple, enough for now) """ - _max_multipart_concurrency = config.get('aws.boto3.max_multipart_concurrency', 16) - _min_pool_connections = 512 - _pool_connections = config.get('aws.boto3.pool_connections', 512) + _max_multipart_concurrency = deferred_config('aws.boto3.max_multipart_concurrency', 16) + _pool_connections = deferred_config('aws.boto3.pool_connections', 512) _stream_download_pool_connections = 128 _stream_download_pool = None @@ -1297,7 +1303,7 @@ class _Boto3Driver(_Driver): Callback=callback, ) except Exception as ex: - log.error('Failed uploading: %s' % ex) + self.get_logger().error('Failed uploading: %s' % ex) return False return True @@ -1310,7 +1316,7 @@ class _Boto3Driver(_Driver): num_download_attempts=container.config.retries), Callback=callback) except Exception as ex: - log.error('Failed uploading: %s' % ex) + self.get_logger().error('Failed uploading: %s' % ex) return False return True @@ -1344,7 +1350,7 @@ class _Boto3Driver(_Driver): try: a_obj.download_fileobj(a_stream, Callback=cb, Config=cfg) except Exception as ex: - log.error('Failed downloading: %s' % ex) + (log or self.get_logger()).error('Failed downloading: %s' % ex) a_stream.close() import boto3.s3.transfer @@ -1366,7 +1372,7 @@ class _Boto3Driver(_Driver): import boto3.s3.transfer p = Path(local_path) if not overwrite_existing and p.is_file(): - log.warning('failed saving after download: overwrite=False and file exists (%s)' % str(p)) + self.get_logger().warning('failed saving after download: overwrite=False and file exists (%s)' % str(p)) return container = self._containers[obj.container_name] obj.download_file(str(p), @@ -1526,7 +1532,7 @@ class _GoogleCloudStorageDriver(_Driver): blob = container.bucket.blob(object_name) blob.upload_from_file(iterator) except Exception as ex: - log.error('Failed uploading: %s' % ex) + self.get_logger().error('Failed uploading: %s' % ex) return False return True @@ -1535,7 +1541,7 @@ class _GoogleCloudStorageDriver(_Driver): blob = container.bucket.blob(object_name) blob.upload_from_filename(file_path) except Exception as ex: - log.error('Failed uploading: %s' % ex) + self.get_logger().error('Failed uploading: %s' % ex) return False return True @@ -1553,7 +1559,7 @@ class _GoogleCloudStorageDriver(_Driver): except ImportError: pass name = getattr(object, "name", "") - log.warning("Failed deleting object {}: {}".format(name, ex)) + self.get_logger().warning("Failed deleting object {}: {}".format(name, ex)) return False return not object.exists() @@ -1572,7 +1578,7 @@ class _GoogleCloudStorageDriver(_Driver): try: a_obj.download_to_file(a_stream) except Exception as ex: - log.error('Failed downloading: %s' % ex) + self.get_logger().error('Failed downloading: %s' % ex) a_stream.close() # return iterable object @@ -1585,7 +1591,7 @@ class _GoogleCloudStorageDriver(_Driver): def download_object(self, obj, local_path, overwrite_existing=True, delete_on_failure=True, callback=None, **_): p = Path(local_path) if not overwrite_existing and p.is_file(): - log.warning('failed saving after download: overwrite=False and file exists (%s)' % str(p)) + self.get_logger().warning('failed saving after download: overwrite=False and file exists (%s)' % str(p)) return obj.download_to_filename(str(p)) @@ -1664,9 +1670,9 @@ class _AzureBlobServiceStorageDriver(_Driver): ) return True except AzureHttpError as ex: - log.error('Failed uploading (Azure error): %s' % ex) + self.get_logger().error('Failed uploading (Azure error): %s' % ex) except Exception as ex: - log.error('Failed uploading: %s' % ex) + self.get_logger().error('Failed uploading: %s' % ex) return False def upload_object(self, file_path, container, object_name, callback=None, extra=None, **kwargs): @@ -1690,9 +1696,9 @@ class _AzureBlobServiceStorageDriver(_Driver): ) return True except AzureHttpError as ex: - log.error('Failed uploading (Azure error): %s' % ex) + self.get_logger().error('Failed uploading (Azure error): %s' % ex) except Exception as ex: - log.error('Failed uploading: %s' % ex) + self.get_logger().error('Failed uploading: %s' % ex) finally: if stream: stream.close() @@ -1727,7 +1733,7 @@ class _AzureBlobServiceStorageDriver(_Driver): container.name, obj.blob_name ) - cb = DownloadProgressReport(total_size_mb, verbose, remote_path, log) + cb = DownloadProgressReport(total_size_mb, verbose, remote_path, self.get_logger()) blob = container.blob_service.get_blob_to_bytes( container.name, obj.blob_name, @@ -1738,7 +1744,7 @@ class _AzureBlobServiceStorageDriver(_Driver): def download_object(self, obj, local_path, overwrite_existing=True, delete_on_failure=True, callback=None, **_): p = Path(local_path) if not overwrite_existing and p.is_file(): - log.warning('failed saving after download: overwrite=False and file exists (%s)' % str(p)) + self.get_logger().warning('failed saving after download: overwrite=False and file exists (%s)' % str(p)) return download_done = threading.Event() diff --git a/clearml/task.py b/clearml/task.py index af2b8a44..1f129f75 100644 --- a/clearml/task.py +++ b/clearml/task.py @@ -50,7 +50,7 @@ from .binding.hydra_bind import PatchHydra from .binding.click_bind import PatchClick from .config import ( config, DEV_TASK_NO_REUSE, get_is_master_node, DEBUG_SIMULATE_REMOTE_TASK, PROC_MASTER_ID_ENV_VAR, - DEV_DEFAULT_OUTPUT_URI, ) + DEV_DEFAULT_OUTPUT_URI, deferred_config, ) from .config import running_remotely, get_remote_task_id from .config.cache import SessionCache from .debugging.log import LoggerRoot @@ -134,9 +134,9 @@ class Task(_Task): __main_task = None # type: Optional[Task] __exit_hook = None __forked_proc_main_pid = None - __task_id_reuse_time_window_in_hours = float(config.get('development.task_reuse_time_window_in_hours', 24.0)) - __detect_repo_async = config.get('development.vcs_repo_detect_async', False) - __default_output_uri = DEV_DEFAULT_OUTPUT_URI.get() or config.get('development.default_output_uri', None) + __task_id_reuse_time_window_in_hours = deferred_config('development.task_reuse_time_window_in_hours', 24.0, float) + __detect_repo_async = deferred_config('development.vcs_repo_detect_async', False) + __default_output_uri = DEV_DEFAULT_OUTPUT_URI.get() or deferred_config('development.default_output_uri', None) class _ConnectedParametersType(object): argparse = "argument_parser" @@ -1987,6 +1987,8 @@ class Task(_Task): # Remove the development system tag system_tags = [t for t in task.get_system_tags() if t != self._development_tag] self.set_system_tags(system_tags) + # if we leave the Task out there, it makes sense to make it editable. + self.reset(force=True) # leave this process. if exit_process: diff --git a/clearml/utilities/pigar/reqs.py b/clearml/utilities/pigar/reqs.py index 6eb31de3..694b70b4 100644 --- a/clearml/utilities/pigar/reqs.py +++ b/clearml/utilities/pigar/reqs.py @@ -316,6 +316,8 @@ def is_std_or_local_lib(name): # if we got here, the loader failed on us, meaning this is definitely a module and not std return False if not module_info: + if name == '__builtin__': + return True return False mpath = module_info.origin # this is std diff --git a/clearml/utilities/proxy_object.py b/clearml/utilities/proxy_object.py index ca5949b6..6517aae9 100644 --- a/clearml/utilities/proxy_object.py +++ b/clearml/utilities/proxy_object.py @@ -195,8 +195,10 @@ class WrapperBase(type): return mtd(*args, **kwargs) return method + typed_class = attrs.get('_base_class_') for name in mcs._special_names: - attrs[name] = make_method(name) + if not typed_class or hasattr(typed_class, name): + attrs[name] = make_method(name) overrides = attrs.get('__overrides__', []) # overrides.extend(k for k, v in attrs.items() if isinstance(v, lazy)) @@ -223,14 +225,68 @@ class LazyEvalWrapper(six.with_metaclass(WrapperBase)): def _remoteref(self): func = object.__getattribute__(self, "_remote_reference") - if func in LazyEvalWrapper._remote_reference_calls: + if func and func in LazyEvalWrapper._remote_reference_calls: LazyEvalWrapper._remote_reference_calls.remove(func) return func() if callable(func) else func + def __getattribute__(self, attr): + if attr in ('__isabstractmethod__', ): + return None + if attr in ('_remoteref', '_remote_reference'): + return object.__getattribute__(self, attr) + return getattr(LazyEvalWrapper._load_object(self), attr) + + def __setattr__(self, attr, value): + setattr(LazyEvalWrapper._load_object(self), attr, value) + + def __delattr__(self, attr): + delattr(LazyEvalWrapper._load_object(self), attr) + + def __nonzero__(self): + return bool(LazyEvalWrapper._load_object(self)) + + def __bool__(self): + return bool(LazyEvalWrapper._load_object(self)) + + @staticmethod + def _load_object(self): + obj = object.__getattribute__(self, "_wrapped") + if obj is None: + cb = object.__getattribute__(self, "_callback") + obj = cb() + object.__setattr__(self, '_wrapped', obj) + return obj + @classmethod def trigger_all_remote_references(cls): for func in cls._remote_reference_calls: if callable(func): func() cls._remote_reference_calls = [] + + +def lazy_eval_wrapper_spec_class(class_type): + class TypedLazyEvalWrapper(six.with_metaclass(WrapperBase)): + _base_class_ = class_type + __slots__ = ['_wrapped', '_callback', '__weakref__'] + + def __init__(self, callback): + object.__setattr__(self, '_wrapped', None) + object.__setattr__(self, '_callback', callback) + + def __nonzero__(self): + return bool(LazyEvalWrapper._load_object(self)) + + def __bool__(self): + return bool(LazyEvalWrapper._load_object(self)) + + def __getattribute__(self, attr): + if attr == '__isabstractmethod__': + return None + if attr == '__class__': + return class_type + + return getattr(LazyEvalWrapper._load_object(self), attr) + + return TypedLazyEvalWrapper