Uh oh!
There was an error while loading. Please reload this page.
Add mcap support to osi3trace - #8
Conversation
TimmRuppert
commented
Sep 16, 2025
Last week, we had an extensive discussion about maintaining 100 % backward compatibility versus introducing a clean new interface through an intentional breaking change. The current version of this PR doesn’t provide the former, while the latter might take too much time to implement (or to agree on an interface). At that time, I mentioned using a factory or some form of delegation pattern. Below is a sketch of the factory approach as a basis for discussion. This should preserve full backward compatibility while still allowing delegation across two subclasses. fromtypingimportOptionalfrompathlibimportPathclassOSITrace:
"""Factory base class that returns appropriate subclass instances based on file format."""def__new__(cls, path: Path=None, type_name: Optional[str] ="SensorView",
cache_messages: Optional[bool] =False, *args, **kwargs):
ifclsisOSITrace: # only run factory when called directlyifpath.suffix.lower() ==".osi":
returnsuper().__new__(SingleOSITrace)
elifpath.suffix.lower() ==".mcap":
returnsuper().__new__(McapOSITrace)
else:
raiseValueError(f"Unsupported trace format detected for: {path}")
returnsuper().__new__(cls)
def__init__(self, path=None, type_name="SensorView", cache_messages=False, *args, **kwargs):
# Base class init does nothingpassclassSingleOSITrace(OSITrace): def__init__(self, path=None, type_name="SensorView", cache_messages=False, *args, **kwargs):
print(f"Initialized SingleOSITrace with path: {path}")
self.path=pathself.type_name=type_nameself.cache_messages=cache_messagesself.member_only_in_single="I am unique to SingleOSITrace"classMcapOSITrace(OSITrace): def__init__(self, path=None, type_name="SensorView", cache_messages=False, *args, **kwargs):
print(f"Initialized McapOSITrace with path: {path}")
self.path=pathself.type_name=type_nameself.start_time=kwargs.get("start_time", None) # unique to McapOSITraceif__name__=="__main__":
print("Creating .osi trace:")
trace1=OSITrace(path=Path("example.osi"))
print("\nCreating .mcap trace:")
trace2=OSITrace(path=Path("example.mcap"), start_time=123)
print("\nChecking types:")
print(f"trace1 type: {type(trace1).__name__}")
print(f"trace2 type: {type(trace2).__name__}")
print(f"trace1 isinstance OSITrace: {isinstance(trace1, OSITrace)}")
print(f"trace2 isinstance OSITrace: {isinstance(trace2, OSITrace)}")
print("\nAccessing some unique values:")
print(f"trace1 member_only_in_single: {trace1.member_only_in_single}")
print(f"trace2 start_time: {trace2.start_time}")
print("\nCreating unsupported trace format:")
trace3=OSITrace(path=Path("example.txt")) # raises ValueErrorExample Output: |
pmai
commented
Sep 16, 2025
I would propose to take a look of integrating either the factory approach or exposing more of the existing interface and potentially also supporting more of the existing interface for the multi-trace file reader. |
thomassedlmayer
commented
Sep 24, 2025
I tried out the factory approach and it worked fine in general and looks cleaner than my original approach when combined with an abstract class for auto completion features. But I did not find a way to allow initializing an empty OSITrace ( For now I continued with the proxy approach and tried to expose the OSITrace legacy attributes by overwriting the attribute getter/setter and forward the calls to the reader in case it's an OSITraceSingle instance. This looks a bit ugly and this also comes with some potential issues I guess but it's the closest I've come to keep the old OSITrace behaviour. |
71449a1 to
8d990b7CompareTimmRuppert
commented
Sep 24, 2025
Although I don’t think this is a great practice for new code, we could apply morphism here. In this case, it seems appropriate since we’re working with an existing codebase and need to expose a specific set of “public” functions/members. Python allows overwriting fromtypingimportOptionalfrompathlibimportPathclassOSITrace:
""" Factory class that returns the correct trace handler (.osi or .mcap) and supports deferred initialization via the .from_file() method. """def__new__(cls, path: Optional[Path] =None, *args, **kwargs):
# If this is a call to a subclass (e.g., SingleOSITrace()), don't use the factory.ifclsisnotOSITrace:
returnsuper().__new__(cls)
# If a path is provided, act as a factory immediately.ifpath:
ifnotisinstance(path, Path): path=Path(path) # Ensure path is a Path objectifpath.suffix.lower() ==".osi":
# Return an instance of the specific subclassreturnsuper().__new__(SingleOSITrace)
elifpath.suffix.lower() ==".mcap":
returnsuper().__new__(McapOSITrace)
else:
raiseValueError(f"Unsupported trace format detected for: {path}")
# If no path is provided, create a base OSITrace instance# that is waiting for the from_file() call.returnsuper().__new__(cls)
def__init__(self, path: Optional[Path] =None, *args, **kwargs):
""" The __init__ of the base class is called after __new__. For subclasses, their own __init__ will handle initialization. For a base instance (path=None), we don't need to do anything here yet. """passdeffrom_file(self, path: Path, type_name: str="SensorView", cache_messages: bool=False, *args, **kwargs):
""" Loads data from a file, morphing the current base instance into the correct subclass based on the file extension. """ifnotisinstance(path, Path): path=Path(path)
target_cls=Noneifpath.suffix.lower() ==".osi":
target_cls=SingleOSITraceelifpath.suffix.lower() ==".mcap":
target_cls=McapOSITraceelse:
raiseValueError(f"Unsupported trace format detected for: {path}")
# === The Magic ===# 1. Change the class of the current instance to the target class.self.__class__=target_cls# 2. Call the __init__ of the new class to properly initialize the instance.target_cls.__init__(self, path=path, type_name=type_name, cache_messages=cache_messages, *args, **kwargs)
classSingleOSITrace(OSITrace):
def__init__(self, path=None, type_name="SensorView", cache_messages=False, *args, **kwargs):
print(f"-> Initialized SingleOSITrace with path: {path}")
self.path=pathself.type_name=type_nameself.cache_messages=cache_messagesself.member_only_in_single="I am unique to SingleOSITrace"classMcapOSITrace(OSITrace):
def__init__(self, path=None, type_name="SensorView", cache_messages=False, *args, **kwargs):
print(f"-> Initialized McapOSITrace with path: {path}")
self.path=pathself.type_name=type_name# Example of subclass-specific argumentself.start_time=kwargs.get("start_time", None)
if__name__=="__main__":
print("--- Scenario 1: Initialization with path (Factory behavior) ---")
print("\nCreating .osi trace directly:")
trace1=OSITrace(path=Path("example.osi"))
print(f"trace1 type: {type(trace1).__name__}")
print(f"trace1 member: {trace1.member_only_in_single}")
print("\nCreating .mcap trace directly:")
trace2=OSITrace(path=Path("example.mcap"), start_time=12345)
print(f"trace2 type: {type(trace2).__name__}")
print(f"trace2 start_time: {trace2.start_time}")
print("\n"+"="*60+"\n")
print("--- Scenario 2: Deferred initialization (Added after your comment) ---")
print("\nCreating empty instance first:")
trace3=OSITrace()
print(f"Initial trace3 type: {type(trace3).__name__}")
print("\nCalling from_file() with .osi path:")
trace3.from_file(path=Path("another.osi"))
print(f"Morphed trace3 type: {type(trace3).__name__}")
print(f"Morphed trace3 member: {trace3.member_only_in_single}")
print(f"Is it still an OSITrace? {isinstance(trace3, OSITrace)}")Output |
asadekasam
commented
Oct 14, 2025
Needs review from the Project Group @pmai@jdsika@msandrk-avl@TimmRuppert . Might be easier to review if we resolve: #9 |
pmai
left a comment
There was a problem hiding this comment.
Generally looks good, however I would make the readers more uniform like shown, which also allows more magic in choosing a topic if none is given. I might push a more complete set of changes - some of which are currently untested - if I get around to it.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Signed-off-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de>
Signed-off-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de>
Signed-off-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de>
Signed-off-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de>
Signed-off-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de>
Signed-off-by: Pierre R. Mai <pmai@pmsf.de>
Add deprecation for purely single-channel only legacy methods, place new methods as reusable base methods, which can potentially be implemented in the future for single-channel as well. Signed-off-by: Pierre R. Mai <pmai@pmsf.de>
8d990b7 to
e45ee9cCompareSigned-off-by: Pierre R. Mai <pmai@pmsf.de>
Signed-off-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de>
Signed-off-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de>
Signed-off-by: Pierre R. Mai <pmai@pmsf.de>
Signed-off-by: Pierre R. Mai <pmai@pmsf.de>
Signed-off-by: Pierre R. Mai <pmai@pmsf.de>
Signed-off-by: Pierre R. Mai <pmai@pmsf.de>
TimmRuppert
left a comment
There was a problem hiding this comment.
Code looks neat and tidy, I did some minor local testing.
Uh oh!
There was an error while loading. Please reload this page.
This PR adds multi-channel trace file support to the existing OSITrace reader class while keeping it backwards compatible in terms of the existing interface/features for single-channel trace files.
The OSITrace class now acts as dispatcher/wrapper for the underlying reader class (OSITraceMulti or OSITraceSingle) which is created depending on the input file type. The functionality of the old OSITrace reader was moved to the OSITraceSingle class (unchanged) and the existing methods of OSITrace are forwarded to the corresponding reader's method depending on feature support.
Note on feature support:
The old single-channel trace reader provides index-based message retrieval while the mcap library does only provides log_time-based message retrieval. This means that existing methods (retrieve_offset, get_message_by_index, etc.) are not supported when reading from an mcap file. On the other hand, metadata features are only supported for mcap. This leaves a kind of unpleasant non-unified interface depending on the underlying file format. The only unified interface for reading both types of file formats is the stateful iterator functionality and the restart and close methods (see
ReaderBaseclass).In the future we might want to unify the features of the single- and multi-channel trace file readers and think about a better interface.