From f5a60c634006dfe93273e55ecc495873815b6a71 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 22 Sep 2024 21:29:05 +0200 Subject: [PATCH] New `JSONDict` class (#16426) Signed-off-by: UltralyticsAssistant Co-authored-by: UltralyticsAssistant --- docs/en/reference/utils/__init__.md | 4 +- ultralytics/utils/__init__.py | 145 ++++++++++++++++------------ 2 files changed, 86 insertions(+), 63 deletions(-) diff --git a/docs/en/reference/utils/__init__.md b/docs/en/reference/utils/__init__.md index 9c5f2b44..9b7d1b58 100644 --- a/docs/en/reference/utils/__init__.md +++ b/docs/en/reference/utils/__init__.md @@ -35,11 +35,11 @@ keywords: Ultralytics, utils, TQDM, Python, ML, Machine Learning utilities, YOLO



-## ::: ultralytics.utils.SettingsManager +## ::: ultralytics.utils.JSONDict



-## ::: ultralytics.utils.PersistentCacheDict +## ::: ultralytics.utils.SettingsManager



diff --git a/ultralytics/utils/__init__.py b/ultralytics/utils/__init__.py index 5ba58d08..a9ada7cd 100644 --- a/ultralytics/utils/__init__.py +++ b/ultralytics/utils/__init__.py @@ -1040,6 +1040,87 @@ def set_sentry(): sentry_sdk.set_user({"id": SETTINGS["uuid"]}) # SHA-256 anonymized UUID hash +class JSONDict(dict): + """ + A dictionary-like class that provides JSON persistence for its contents. + + This class extends the built-in dictionary to automatically save its contents to a JSON file whenever they are + modified. It ensures thread-safe operations using a lock. + + Attributes: + file_path (Path): The path to the JSON file used for persistence. + lock (threading.Lock): A lock object to ensure thread-safe operations. + + Methods: + _load: Loads the data from the JSON file into the dictionary. + _save: Saves the current state of the dictionary to the JSON file. + __setitem__: Stores a key-value pair and persists it to disk. + __delitem__: Removes an item and updates the persistent storage. + update: Updates the dictionary and persists changes. + clear: Clears all entries and updates the persistent storage. + + Examples: + >>> json_dict = JSONDict("data.json") + >>> json_dict["key"] = "value" + >>> print(json_dict["key"]) + value + >>> del json_dict["key"] + >>> json_dict.update({"new_key": "new_value"}) + >>> json_dict.clear() + """ + + def __init__(self, file_path: Union[str, Path] = "data.json"): + """Initialize a JSONDict object with a specified file path for JSON persistence.""" + super().__init__() + self.file_path = Path(file_path) + self.lock = Lock() + self._load() + + def _load(self): + """Load the data from the JSON file into the dictionary.""" + try: + if self.file_path.exists(): + with open(self.file_path) as f: + self.update(json.load(f)) + except json.JSONDecodeError: + print(f"Error decoding JSON from {self.file_path}. Starting with an empty dictionary.") + except Exception as e: + print(f"Error reading from {self.file_path}: {e}") + + def _save(self): + """Save the current state of the dictionary to the JSON file.""" + try: + self.file_path.parent.mkdir(parents=True, exist_ok=True) + with open(self.file_path, "w") as f: + json.dump(dict(self), f, indent=2) + except Exception as e: + print(f"Error writing to {self.file_path}: {e}") + + def __setitem__(self, key, value): + """Store a key-value pair and persist to disk.""" + with self.lock: + super().__setitem__(key, value) + self._save() + + def __delitem__(self, key): + """Remove an item and update the persistent storage.""" + with self.lock: + super().__delitem__(key) + self._save() + + def update(self, *args, **kwargs): + """Update the dictionary and persist changes.""" + with self.lock: + super().update(*args, **kwargs) + self._save() + + def clear(self): + """Clear all entries and update the persistent storage.""" + with self.lock: + super().clear() + self._save() + + class SettingsManager(dict): """ Manages Ultralytics settings stored in a YAML file. @@ -1138,61 +1219,6 @@ class SettingsManager(dict): self.save() -class PersistentCacheDict(dict): - """A thread-safe dictionary that persists data to a JSON file for caching purposes.""" - - def __init__(self, file_path=USER_CONFIG_DIR / "persistent_cache.json"): - """Initializes a thread-safe persistent cache dictionary with a specified file path for storage.""" - super().__init__() - self.file_path = Path(file_path) - self.lock = Lock() - self._load() - - def _load(self): - """Load the persistent cache from a JSON file into the dictionary, handling errors gracefully.""" - try: - if self.file_path.exists(): - with open(self.file_path) as f: - self.update(json.load(f)) - except json.JSONDecodeError: - print(f"Error decoding JSON from {self.file_path}. Starting with an empty cache.") - except Exception as e: - print(f"Error reading from {self.file_path}: {e}") - - def _save(self): - """Save the current state of the cache dictionary to a JSON file, ensuring thread safety.""" - try: - self.file_path.parent.mkdir(parents=True, exist_ok=True) - with open(self.file_path, "w") as f: - json.dump(dict(self), f, indent=2) - except Exception as e: - print(f"Error writing to {self.file_path}: {e}") - - def __setitem__(self, key, value): - """Store a key-value pair in the cache and persist the updated cache to disk.""" - with self.lock: - super().__setitem__(key, value) - self._save() - - def __delitem__(self, key): - """Remove an item from the PersistentCacheDict and update the persistent storage.""" - with self.lock: - super().__delitem__(key) - self._save() - - def update(self, *args, **kwargs): - """Update the dictionary with key-value pairs from other mappings or iterables, ensuring thread safety.""" - with self.lock: - super().update(*args, **kwargs) - self._save() - - def clear(self): - """Clears all entries from the persistent cache dictionary, ensuring thread safety.""" - with self.lock: - super().clear() - self._save() - - def deprecation_warn(arg, new_arg): """Issue a deprecation warning when a deprecated argument is used, suggesting an updated argument.""" LOGGER.warning( @@ -1216,11 +1242,8 @@ def vscode_msg(ext="ultralytics.ultralytics-snippets") -> str: path = (USER_CONFIG_DIR.parents[2] if WINDOWS else USER_CONFIG_DIR.parents[1]) / ".vscode/extensions" obs_file = path / ".obsolete" # file tracks uninstalled extensions, while source directory remains installed = any(path.glob(f"{ext}*")) and ext not in (obs_file.read_text("utf-8") if obs_file.exists() else "") - return ( - "" - if installed - else f"{colorstr('VS Code:')} view Ultralytics VS Code Extension ⚡ at https://docs.ultralytics.com/integrations/vscode" - ) + url = "https://docs.ultralytics.com/integrations/vscode" + return "" if installed else f"{colorstr('VS Code:')} view Ultralytics VS Code Extension ⚡ at {url}" # Run below code on utils init ------------------------------------------------------------------------------------ @@ -1228,7 +1251,7 @@ def vscode_msg(ext="ultralytics.ultralytics-snippets") -> str: # Check first-install steps PREFIX = colorstr("Ultralytics: ") SETTINGS = SettingsManager() # initialize settings -PERSISTENT_CACHE = PersistentCacheDict() # initialize persistent cache +PERSISTENT_CACHE = JSONDict(USER_CONFIG_DIR / "persistent_cache.json") # initialize persistent cache DATASETS_DIR = Path(SETTINGS["datasets_dir"]) # global datasets directory WEIGHTS_DIR = Path(SETTINGS["weights_dir"]) # global weights directory RUNS_DIR = Path(SETTINGS["runs_dir"]) # global runs directory