Geskriptete Objekte werden jedes Mal neu aufgebaut, wenn ein FCStd Dokument geöffnet wird. Zu diesem Zweck behält das Dokument eine Referenz auf das Modul und die Python Klasse, die zur Erstellung des Objekts verwendet wurden, zusammen mit seinen Eigenschaften.
<Document SchemaVersion="4" ProgramVersion="0.19R20959 (Git)" FileVersion="1">
...
<Properties Count="15" TransientCount="3">
...
</Properties>
<Objects Count="1" Dependencies="1">
<ObjectDeps Name="Custom" Count="0"/>
<Object type="Part::FeaturePython" name="Custom" id="2715" Touched="1" />
</Objects>
<ObjectData Count="1">
<Object name="Custom">
<Properties Count="9" TransientCount="0">
...
<Property name="Proxy" type="App::PropertyPythonObject" status="1">
<Python value="eyJUeXBlIjogIkN1c3RvbSJ9" encoded="yes" module="old_module" class="OldObject"/>
</Property>
...
</Properties>
</Object>
</ObjectData>
</Document>
Konzentriere dich besonders auf diesen Teil:
...
<Property name="Proxy" type="App::PropertyPythonObject" status="1">
<Python value="eyJUeXBlIjogIkN1c3RvbSJ9" encoded="yes" module="old_module" class="OldObject"/>
</Property>
...
Wenn der Wert von module= oder class= auf dem installierten System nicht gefunden wird, kann das Objekt nicht korrekt geladen werden. Das bedeutet, dass, sobald ein Objekt mit einer bestimmten Klasse erstellt wurde, das Modul nicht mehr verschoben oder umbenannt werden sollte, da zuvor gespeicherte Objekte sonst kaputt gehen.
Ein triftiger Grund für die Verschiebung oder Umbenennung des Moduls oder der Klasse ist jedoch die Verbesserung der Struktur und Wartbarkeit des ursprünglichen Codes, z.B. bei der Umstrukturierung einer ganzen Werkbank. In diesem Fall gibt es verschiedene Strategien, um alte Objekte auf die Verwendung einer neuen Klasse zu migrieren. Dies geschieht aus Gründen der Abwärtskompatibilität, wenn ein völliges Aufbrechen alter Dokumente vermieden werden muss.
Ein altes Objekt wird in einem Baustein definiert, der sich an der Wurzel der Arbeitsbereichs befindet.
# old_module.py
class OldObject:
def __init__(self, obj):
obj.addProperty("App::PropertyLength", "Length")
obj.addProperty("App::PropertyArea", "Area")
obj.Length = 15
obj.Area = 300
obj.Proxy = self
self.Type = "Custom"
def execute(self, obj):
pass
Mit dieser Klasse kann ein Objekt erstellt und unter mein_Dokument.FCstd gespeichert werden. Wenn dem neuen Objekt kein bestimmter Ansichtsanbieter zugewiesen ist, wird seine Proxy Klasse einfach auf einen anderen Wert als None gesetzt, in diesem Fall auf 1.
import FreeCAD as App
import old_module
doc = App.newDocument()
doc.FileName = "my_document.FCStd"
obj = doc.addObject("Part::FeaturePython", "Custom")
old_module.OldObject(obj)
if App.GuiUp:
obj.ViewObject.Proxy = 1
doc.recompute()
doc.save()
Python Konsole Sitzung, bei der die grundlegenden Eigenschaften weggelassen wurden.
>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Area', ..., ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj.Proxy)
<old_module.OldObject object at 0x7efc3c51c390>
Nun betrachten wir, dass der Arbeitsbereich umstrukturiert wird, so dass sich die Klassen nicht nur im Stammverzeichnis, sondern stattdessen in einem objects Verzeichnis befinden. Komplexe Arbeitsbereiche, die viele verschiedene Arten von Objekten haben, sollten in Verzeichnissen strukturiert werden, die Objekte, AnsichtBereitsteller, Gui Befehle, Aufgabenpaneel Schnittstellen usw. enthalten.
# objects/new_module.py
class NewObject:
def __init__(self, obj):
obj.addProperty("App::PropertyLength", "Length")
obj.addProperty("App::PropertyArea", "GeneralArea")
obj.addProperty("App::PropertyInteger", "Divisions")
obj.Length = 30
obj.GeneralArea = 600
obj.Divisions = 4
obj.Proxy = self
self.Type = "Custom"
def execute(self, obj):
pass
Diese neue Klasse wird sich auf den gleichen Objekttyp beziehen, aber sowohl der Modulname als auch der Klassenname wurden umbenannt. Darüber hinaus haben sich auch die Eigenschaften geändert; eine Eigenschaft wurde umbenannt, und eine völlig neue Eigenschaft wurde hinzugefügt.
Wenn wir ein neues Objekt mit diesem neuen Modul erstellen, haben wir die folgende Konsolensitzung.
>>> obj2 = App.ActiveDocument.Custom2
>>> print(obj2.PropertiesList)
['Divisions', ..., 'GeneralArea', ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj2.Proxy)
<objects.new_module.NewObject object at 0x7efc1cf68c50>
Wir werden das ältere Objekt migrieren, indem wir die alte Klasse umleiten. Die ursprüngliche Klasse wird gelöscht, und der Name der Klasse wird einfach umgeleitet, um auf die neue Klasse zu verweisen.
# old_module.py
import objects.new_module as new_module
OldObject = new_module.NewObject
Jedes Dokument, das versucht, old_module.OldObject zu laden, wird stattdessen zum Laden von objects.new_module.NewObject umgeleitet.
Wenn wir das Dokument öffnen und die Eigenschaften des Objekts in der Python Konsole überprüfen, werden wir sehen, dass die älteren Eigenschaften erhalten bleiben, das Objekt aber eine neue Proxy Klasse hat.
>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Area', ..., ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj.Proxy)
<objects.new_module.NewObject object at 0x7f099700b2b0>
In diesem Fall sehen wir jedoch nicht die neuen Eigenschaften der neuen Klasse. Der Grund dafür ist einfach, dass das ältere Objekt diese Eigenschaften nicht hatte. Wenn old_module.OldObject zu objects.new_module.NewObject umgeleitet wurde, änderte sich nur die Proxy Klasse, aber die vorherigen Informationen wurden beibehalten.
Wenn das Dokument nun gespeichert und wieder geöffnet wird, sucht es automatisch nach objects.new_module.NewObject, und es benötigt nicht mehr old_module.OldObject. Die Datei old_module.py kann dauerhaft aus dem System entfernt werden, solange alle älteren Objekte in das neue Modul migriert worden sind. Wenn das alte Modul entfernt wird, aber ein Objekt nicht migriert wurde, zeigt die Berichtsansicht beim Öffnen eines Dokuments, das ein solches Objekt enthält, eine Meldung wie diese an.
<class 'ModuleNotFoundError'>: No module named 'old_module'
Wenn es realistischerweise nicht möglich ist, alle älteren Objekte zu migrieren, z.B. weil das alte Modul viele Jahre lang in einem Arbeitsbereich verwendet wurde, muss old_module.py so lange beibehalten werden, wie es für notwendig erachtet wird, um den Benutzern die Möglichkeit zu geben, ihre Objekte zu migrieren.
Vorteile
Nachteile
Wir werden das ältere Objekt migrieren, indem wir die alte Klasse modifizieren. Der Großteil der ursprünglichen Klasse wird gelöscht, und stattdessen wird die Methode onDocumentRestored implementiert. Wenn diese Methode vorhanden ist, wird sie ausgeführt, wenn das Dokument versucht, ein Objekt wiederherzustellen, das die Klasse verwendet. Dies ist also die Gelegenheit, eine neue Klasse zuzuweisen, die Informationen zu manipulieren oder Nachrichten zu drucken.
In diesem Fall nehmen wir an, dass wir auch einen neuen AnsichtBereitsteller im Modul viewp/new_view.py definiert haben. Wenn wir diese Klasse nicht migrieren wollen, können wir nach der Prüfung App.GuiUp alles weglassen.
# old_module.py
import FreeCAD as App
import objects.new_module as new_module
import viewp.new_view as new_view
_wrn = App.Console.PrintWarning
class OldObject:
def onDocumentRestored(self, obj):
new_module.NewObject(obj)
_wrn("New proxy class used\n")
if App.GuiUp:
new_view.ViewProviderNew(obj.ViewObject)
_wrn("New viewprovider class used\n")
In einem komplexeren Beispiel wird zunächst geprüft, ob die Proxy Klasse dem gesuchten Typ entspricht, und erst dann mit der Migration fortgefahren, wenn es sich um den richtigen Typ handelt.
class OldObject:
def onDocumentRestored(self, obj):
if hasattr(obj, "Proxy") and obj.Proxy.Type == "Custom":
_module = str(obj.Proxy.__class__)
_module = _module.lstrip("<class '").rstrip("'>")
if _module == "old_module.OldObject":
self._migrate(obj)
def _migrate(self, obj):
_wrn("New proxy class used\n")
new_module.NewObject(obj)
if App.GuiUp:
new_view.ViewProviderNew(obj.ViewObject)
_wrn("New viewprovider class used\n")
Wenn wir davon ausgehen, dass wir das alte Modul bereits auf diese Weise geändert haben, werden wir, wenn wir ein Dokument mit einem alten Objekt öffnen, die Meldungen sehen, in denen die Verwendung der neuen Klassen erwähnt wird.
Wenn wir das Objekt von der Python Konsole aus inspizieren, werden wir sehen, dass die älteren Eigenschaften erhalten geblieben sind, und zusätzlich wurden zusammen mit der neuen Proxy Klasse neue Eigenschaften hinzugefügt.
>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Area', 'Divisions', ..., 'GeneralArea', ..., ..., 'Length', 'Length1', ..., ..., ..., ...]
>>> print(obj.Proxy)
<objects.new_module.NewObject object at 0x7fecb0ebd7b8>
Die alten Eigenschaften waren Area und Length; die neuen Eigenschaften sind Divisions, GeneralArea und Length. Das migrierte Objekt behält die beiden ursprünglichen Eigenschaften bei und erhält drei neue Eigenschaften. Da die neue Length jedoch denselben Namen wie die ältere Eigenschaft hat, wird die neue Eigenschaft mit einer inkrementellen Nummer umbenannt. Vermutlich ist dies nicht das, was wir wollen. Wir können die Situation verbessern, indem wir dem Zusatz 2.1 unten folgen.
Da die Klassen den gleichen Objekttyp behandeln sollen, wünschen wir uns eine Migration, bei der sich Area in GeneralArea verwandelt und Length einfach dem neuen Length zugewiesen wird und es keine doppelten Eigenschaften gibt.
Vorteile
Nachteile
onDocumentRestored implementieren müssen, um das Objekt zu migrieren.
Dies ist eine Erweiterung von Methode 2. In der Methode onDocumentRestored müssen wir die Werte der gewünschten Eigenschaften speichern und können dann diese ursprünglichen Eigenschaften entfernen. Dies geschieht, damit bei Verwendung der neuen Klasse die neuen Eigenschaften zugewiesen werden, ohne dass es zu Namenskonflikten mit den älteren Eigenschaften kommt.
Wie in Methode 2 können wir auch hier einen Code hinzufügen, der überprüft, ob die Proxy-Klasse die richtige ist. In diesem Beispiel gehen wir erneut davon aus, dass wir einen benutzerdefinierten Viewprovider mit mindestens einer benutzerdefinierten Eigenschaft verwenden.
# old_module.py
import FreeCAD as App
import objects.new_module as new_module
import viewp.new_view as new_view
_wrn = App.Console.PrintWarning
class OldObject:
def onDocumentRestored(self, obj):
old = dict()
old["Area"] = obj.Area
old["Length"] = obj.Length
obj.removeProperty("Area")
obj.removeProperty("Length")
new_module.NewObject(obj)
obj.GeneralArea = 3 * old["Area"]
obj.Length = old["Length"]
_wrn("New proxy class used; properties migrated\n")
if App.GuiUp:
vobj = obj.ViewObject
old = dict()
old["LineScale"] = vobj.LineScale
vobj.removeProperty("LineScale")
new_view.ViewProviderNew(vobj)
vobj.LineScale = 1.05 * old["LineScale"]
_wrn("New viewprovider class used; view properties migrated\n")
Wir sehen, dass die alten Werte in einem Hilfswörterbuch gespeichert werden, dann werden die alten Eigenschaften entfernt, dann fügen wir die neue Klasse hinzu und schließlich weisen wir die zuvor gespeicherten Werte den neuen Eigenschaften zu. In diesem Moment können wir die gespeicherten Werte nach Bedarf für die neue Klasse transformieren. Beispielsweise wird GeneralArea auf das Dreifache des alten Area gesetzt, und das neue Length erhält einfach den Wert des alten Length. Da wir wissen, wie sich die alten und neuen Klassen verhalten sollen, haben wir die Freiheit, die Daten so zu manipulieren, dass das Objekt nach unseren Wünschen migriert wird.
Wir können nur Eigenschaften entfernen, die von Python-Klassen hinzugefügt wurden, als wir das skriptgenerierte Objekt gebaut haben. Andere Attribute gehören zum Basis-C++-Objekt und können nicht entfernt werden.
>>> obj.removeProperty("Visibility")
False
Angenommen, wir haben das alte Modul bereits auf diese Weise geändert, dann sehen wir beim Öffnen eines Dokuments mit einem alten Objekt die Meldungen, die auf die Verwendung der neuen Klassen hinweisen. Bei der Überprüfung des Objekts in der Python-Konsole stellen wir fest, dass die älteren Eigenschaften entfernt wurden und nur noch die neuen Eigenschaften vorhanden sind.
>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Divisions', ..., 'GeneralArea', ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj.Proxy)
<objects.new_module.NewObject object at 0x7efd456c9b00>
Da in der alten Klasse die Eigenschaft Divisions nicht existierte, wurde nichts damit gemacht. Sie wurde einfach von der neuen Klasse objects.new_module.NewObject erstellt.
Vorteile
Nachteile
onDocumentRestored implementieren und jede der Eigenschaften einzeln behandeln müssen (Wert speichern, Eigenschaft löschen, Wert neu zuweisen). Dies ist problematisch, wenn das Objekt, das wir migrieren möchten, viele Eigenschaften hat oder deren Werte auf ganz besondere Weise transformiert werden müssen.
Einer der Nachteile von Methode 2 ist, dass sie immer versucht, die neuen Eigenschaften hinzuzufügen. Wenn die älteren Eigenschaften denselben Namen wie die neuen Eigenschaften haben, werden sie mit einer fortlaufenden Nummer dupliziert, sodass Length zu Length1, dann zu Length2 usw. führt. Dies macht Methode 2 in den meisten Fällen zu einer unrealistischen Option, da die neue Klasse ohnehin nur eine Eigenschaft verwenden wird.
Um diese Methode zu verbessern, kann die neue Klasse auch so geändert werden, dass die Eigenschaften nur hinzugefügt werden, wenn sie nicht bereits unter demselben Namen vorhanden sind.
# objects/new_module.py
class NewObject:
def __init__(self, obj):
if not hasattr(obj, "Length"):
obj.addProperty("App::PropertyLength", "Length")
obj.Length = 30
if not hasattr(obj, "GeneralArea"):
obj.addProperty("App::PropertyArea", "GeneralArea")
obj.GeneralArea = 600
if not hasattr(obj, "Divisions"):
obj.addProperty("App::PropertyInteger", "Divisions")
obj.Divisions = 4
obj.Proxy = self
self.Type = "Custom"
def execute(self, obj):
pass
In diesem Fall wird Length nicht erneut hinzugefügt, da es bereits vorhanden ist. GeneralArea und Divisions sind nicht vorhanden und werden daher hinzugefügt. Und wie zuvor bleibt Area erhalten, da es nicht explizit entfernt wird, obwohl es in der neuen Klasse möglicherweise nicht mehr verwendet wird.
>>> obj = App.ActiveDocument.Custom
>>> print(obj.PropertiesList)
['Area', 'Divisions', ..., 'GeneralArea', ..., ..., 'Length', ..., ..., ..., ...]
>>> print(obj.Proxy)
<objects.new_module.NewObject object at 0x7f036bd4c6a0>
Das Gleiche kann für die Klasse der Viewprovider getan werden.
Bei Verwendung dieser Methode 2 + A ist das Ergebnis ähnlich wie bei Methode 1, da das Objekt alle bisherigen Eigenschaften beibehält, aber zusätzlich die neuen Eigenschaften der neuen Klasse erhält.
Methode 3 benötigt diesen Zusatz zur neuen Klasse nicht, da die älteren Eigenschaften explizit entfernt werden, sodass es bei der Installation der neuen Eigenschaften zu keinen Konflikten kommt. Dennoch ist es nach wie vor empfehlenswert, dass jede Klasse ihre erforderlichen Eigenschaften nur dann hinzufügt, wenn diese noch nicht vorhanden sind. Dies ist sowohl bei der Erstellung neuer Skriptgenerierte Objekte oder bei ihrer Migration.
Vorteile
Nachteile
Methode 3 ist die komplexeste Methode, da die Eigenschaften einzeln behandelt werden. Allerdings haben wir bei dieser Methode auch volle Flexibilität bei der Bearbeitung der Daten, was ein Vorteil ist, wenn wir komplexe Operationen durchführen möchten.
Wenn wir von Anfang an eine Eigenschaft erstellen, die die Versionsnummer unseres Objekts enthält, können wir diese Nummer in Zukunft verwenden, um eine bestimmte Migration von dieser Version zu einer anderen durchzuführen. Wir legen die Eigenschaft als schreibgeschützt fest, sodass wir sie in der Eigenschaften-Ansicht nicht überschreiben können, obwohl sie weiterhin über die Python-Konsole zugänglich sind.
# old_module.py
class OldObject:
def __init__(self, obj):
obj.addProperty("App::PropertyLength", "Length")
obj.addProperty("App::PropertyArea", "Area")
obj.addProperty("App::PropertyString", "Version")
obj.setEditorMode("Version", 1)
obj.Length = 15
obj.Area = 300
obj.Version = "0.18"
obj.Proxy = self
self.Type = "Custom"
def execute(self, obj):
pass
Wenn wir dann das Objekt migrieren möchten, implementieren wir die Methode onDocumentRestored und testen diese Version.
# old_module.py
import FreeCAD as App
import objects.new_module as new_module
_wrn = App.Console.PrintWarning
class OldObject:
def onDocumentRestored(self, obj):
if hasattr(obj, "Version") and obj.Version:
if obj.Version == "0.18":
_migrate_from_018(obj)
elif obj.Version == "0.19":
_migrate_from_019(obj)
def _migrate_from_018(obj):
old = dict()
old["Area"] = obj.Area
old["Length"] = obj.Length
obj.removeProperty("Area")
obj.removeProperty("Length")
obj.removeProperty("Version")
new_module.NewObject(obj)
obj.GeneralArea = 3 * old["Area"]
obj.Length = old["Length"]
obj.Version = "0.20"
_wrn("New proxy class used; properties migrated\n")
def _migrate_from_019(obj):
...
Wir speichern den Wert Version nicht, da wir bei der Migration eine neue Version-Nummer festlegen werden. Wie im Beispiel gezeigt, können wir verschiedene Funktionen für jede entsprechende Version des Objekts implementieren, das wir migrieren möchten. Wir lassen die Migration der Viewprovider-Eigenschaften weg, aber sie folgt dem gleichen Muster.
Vorteile
Nachteile
Anstatt eine Eigenschaft des Objekts zum Speichern der Versionsinformationen zu verwenden, können wir ein Attribut der Klasse verwenden. Auf diese Weise "verbergen" wir die Versionsinformationen, da Eigenschaften normalerweise öffentlich sind und im Eigenschaftseditor angezeigt werden, während Klassenattribute nur über die Python-Konsole bearbeitet werden können. Klassenattribute können wie unter Geskriptete Objekte die Attribute speichern beschrieben gespeichert und wiederhergestellt werden.
# old_module.py
class OldObject:
def __init__(self, obj):
obj.addProperty("App::PropertyLength", "Length")
obj.addProperty("App::PropertyArea", "Area")
obj.Length = 15
obj.Area = 300
obj.Proxy = self
self.Type = "Custom"
self.ver = "0.18"
def execute(self, obj):
pass
Dieses Attribut wird durch Überprüfen des Attributs Proxy überprüft.
>>> obj = App.ActiveDocument.Custom
>>> print(obj.Proxy.ver)
0.18
Anschließend wird die Datei geändert, um das Objekt zu migrieren.
# old_module.py
import FreeCAD as App
import objects.new_module as new_module
_wrn = App.Console.PrintWarning
class OldObject:
def onDocumentRestored(self, obj):
if hasattr(obj.Proxy, "ver") and obj.Proxy.ver:
if obj.Proxy.ver == "0.18":
_migrate_from_018(obj)
def _migrate_from_018(obj):
old = dict()
old["Area"] = obj.Area
old["Length"] = obj.Length
obj.removeProperty("Area")
obj.removeProperty("Length")
new_module.NewObject(obj)
obj.GeneralArea = 3 * old["Area"]
obj.Length = old["Length"]
_wrn("New proxy class used; properties migrated\n")
Wenn wir die neue Klasse installieren, sollte diese neue Klasse den neuen Wert des Versionsattributs festlegen, zum Beispiel self.ver = "0.20".
Wie in Anhang A können wir die neue Klasse so schreiben, dass Eigenschaften nur dann erstellt werden, wenn sie noch nicht vorhanden sind. Mit Methode 3 speichern wir die Werte der älteren Eigenschaften und löschen anschließend die älteren Eigenschaften. Wenn die neuen Eigenschaften jedoch denselben Namen wie die älteren haben, müssen wir die älteren nicht löschen, sondern können einfach dieselbe Eigenschaft wiederverwenden, da wir wissen, dass die Eigenschaft nicht dupliziert wird. Wenn wir Anhang B verwenden, haben wir auch die Möglichkeit, die Version abzufragen.
# old_module.py
import FreeCAD as App
import objects.new_module as new_module
_wrn = App.Console.PrintWarning
class OldObject:
def onDocumentRestored(self, obj):
if hasattr(obj, "Version") and obj.Version:
if obj.Version == "0.18":
_migrate_from_018(obj)
def _migrate_from_018(obj):
old = dict()
old["Area"] = obj.Area
obj.removeProperty("Area")
new_module.NewObject(obj)
obj.GeneralArea = 3 * old["Area"]
obj.Version = "0.20"
_wrn("New proxy class used; properties migrated\n")
Wie wir im Beispiel sehen, wird die alte Eigenschaft Area gelöscht und wie üblich in die neue Eigenschaft GeneralArea migriert. Wir müssen weder Length noch Version löschen, da sie in der neuen Klasse weiterhin unter dem gleichen Namen verwendet werden und nicht erneut erstellt werden (Anhang A). Da wir Length nicht ändern möchten, wird diese Eigenschaft überhaupt nicht verändert, sondern stillschweigend in die neue Klasse migriert. Wir aktualisieren jedoch Version auf den neuen Wert. Wir lassen die Migration der Viewprovider-Eigenschaften weg, aber sie folgt dem gleichen Muster.
Dies sollte wie Methode 3 funktionieren, d. h. die alten Eigenschaften werden entfernt und nur die neuen Eigenschaften bleiben im neuen Objekt erhalten. Der einzige Unterschied besteht darin, dass wir das Entfernen und Neuerstellen der Eigenschaften mit dem gleichen Namen weglassen. Dieser Vorgang sollte funktionieren, solange die alte Eigenschaft und die neue Eigenschaft denselben Typ haben (z. B. App::PropertyLength oder App::PropertyArea), sodass die alte Eigenschaft ihren Wert direkt weitergeben kann. Wenn die neue Eigenschaft jedoch einen anderen Typ als die alte Eigenschaft hat, sollte die alte Eigenschaft entfernt werden, da sie sonst die neue Eigenschaft vollständig überschreibt, was wahrscheinlich nicht gewünscht ist, da die neue Klasse den neuen Typ und nicht den alten Typ erwartet.
Vorteile
Nachteile
Jede der Methoden hat eine empfohlene Anwendung:
Vermeide vorzugsweise Folgendes: