From 9ab77dee4248ec1013150e5dd2518ef086c5d837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ko=C5=88a=C5=99=C3=ADk?= Date: Wed, 17 Dec 2025 17:12:45 +0100 Subject: [PATCH 1/2] Change MerginProject constructor to force one instance per project --- mergin/merginproject.py | 26 +++++++++++++++++++++++++- setup.py | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/mergin/merginproject.py b/mergin/merginproject.py index 9013651..0b9bf13 100644 --- a/mergin/merginproject.py +++ b/mergin/merginproject.py @@ -4,8 +4,9 @@ import os import re import shutil -import uuid import tempfile +import uuid +import weakref from datetime import datetime from dateutil.tz import tzlocal @@ -41,7 +42,30 @@ class MerginProject: Linked to existing local directory, with project metadata (mergin.json) and backups located in .mergin directory. """ + # To make sure we don't have multiple instances for a single project that + # then have out-of-date information, we keep this map of absolute project + # directory paths to instances. + # The dictionary is a WeakValueDictionary so we don't cause memory leaks + # (when the instance is GC'd, the entry is deleted). + project_cache: weakref.WeakValueDictionary[str, "MerginProject"] = weakref.WeakValueDictionary() + + def __new__(cls, directory): + directory = os.path.abspath(directory) + if instance := cls.project_cache.get(directory): + return instance + + instance = super().__new__(cls) + cls.project_cache[directory] = instance + return instance + def __init__(self, directory): + # __init__ still gets called after __new__, even if it returns a + # pre-existing object. Work around this by checking whether we've been + # initialised. + if hasattr(self, "_initialised"): + return + self._initialised = True + self.dir = os.path.abspath(directory) if not os.path.exists(self.dir): raise InvalidProject("Project directory does not exist") diff --git a/setup.py b/setup.py index da2d05a..a8184e6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="mergin-client", - version="0.11.0", + version="0.12.0", url="https://github.com/MerginMaps/python-api-client", license="MIT", author="Lutra Consulting Ltd.", From f4a3cf67e7141e227b8ac691896ff4fc3992edbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ko=C5=88a=C5=99=C3=ADk?= Date: Fri, 19 Dec 2025 11:55:32 +0100 Subject: [PATCH 2/2] Apply changes from review --- mergin/merginproject.py | 11 ++++++----- setup.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mergin/merginproject.py b/mergin/merginproject.py index 0b9bf13..fc3d5a7 100644 --- a/mergin/merginproject.py +++ b/mergin/merginproject.py @@ -51,12 +51,13 @@ class MerginProject: def __new__(cls, directory): directory = os.path.abspath(directory) - if instance := cls.project_cache.get(directory): + instance = cls.project_cache.get(directory) + if instance: + return instance + else: + instance = super().__new__(cls) + cls.project_cache[directory] = instance return instance - - instance = super().__new__(cls) - cls.project_cache[directory] = instance - return instance def __init__(self, directory): # __init__ still gets called after __new__, even if it returns a diff --git a/setup.py b/setup.py index a8184e6..da2d05a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="mergin-client", - version="0.12.0", + version="0.11.0", url="https://github.com/MerginMaps/python-api-client", license="MIT", author="Lutra Consulting Ltd.",