sysmlpy 0.81.0__tar.gz → 0.83.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/PKG-INFO +1 -1
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/pyproject.toml +1 -1
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/__init__.py +2 -2
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/__main__.py +25 -0
- sysmlpy-0.83.0/src/sysmlpy/diff.py +541 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/server.py +466 -46
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/usage.py +9 -0
- sysmlpy-0.81.0/src/sysmlpy/diff.py +0 -280
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/LICENSE +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/README.md +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/README.md +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Lexer.interp +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Lexer.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Lexer.tokens +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Parser.interp +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Parser.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2Parser.tokens +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2ParserListener.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/SysMLv2ParserVisitor.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr_parser.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/antlr_visitor.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/boxes_view.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/definition.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/evaluator.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/examples/attribute_values.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/examples/part_attributes.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/examples/tuples_sequences.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/formatting.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/README.md +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.g4 +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.g4 +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.interp +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2Parser.tokens +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2ParserListener.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/SysMLv2ParserVisitor.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/desc.xml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/examples/camera.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/examples/toaster-system.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/examples/vehicle-model.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/antlr4/pom.xml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/grammar/classes.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/interchange.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/LICENSE +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/README.md +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/__init__.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Analysis/AnalysisTooling.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Analysis/SampledFunctions.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Analysis/StateSpaceRepresentation.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Analysis/TradeStudies.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Cause and Effect/CausationConnections.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Cause and Effect/CauseAndEffect.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Geometry/ShapeItems.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Geometry/SpatialItems.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Metadata/ImageMetadata.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Metadata/ModelingMetadata.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Metadata/ParametersOfInterestMetadata.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Metadata/RiskMetadata.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQ.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQAcoustics.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQAtomicNuclear.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQBase.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQCharacteristicNumbers.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQChemistryMolecular.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQCondensedMatter.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQElectromagnetism.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQInformation.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQLight.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQMechanics.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQSpaceTime.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/ISQThermodynamics.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/MeasurementRefCalculations.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/MeasurementReferences.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/Quantities.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/QuantityCalculations.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/SI.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/SIPrefixes.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/TensorCalculations.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/Time.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/USCustomaryUnits.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Quantities and Units/VectorCalculations.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Requirement Derivation/DerivationConnections.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/domain/Requirement Derivation/RequirementDerivation.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Base.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/BaseFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/BooleanFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Clocks.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/CollectionFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Collections.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ComplexFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ControlFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ControlPerformances.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/DataFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/FeatureReferencingPerformances.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/IntegerFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/KerML.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Links.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Metaobjects.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/NaturalFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/NumericalFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Objects.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Observation.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/OccurrenceFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Occurrences.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Performances.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/RationalFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/RealFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ScalarFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/ScalarValues.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/SequenceFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/SpatialFrames.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/StatePerformances.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/StringFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Transfers.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/TransitionPerformances.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/TrigFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/Triggers.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/VectorFunctions.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/kernel/VectorValues.kerml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Actions.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Allocations.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/AnalysisCases.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Attributes.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Calculations.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Cases.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Connections.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Constraints.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Flows.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Interfaces.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Items.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Metadata.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Parts.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Ports.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Requirements.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/StandardViewDefinitions.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/States.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/SysML.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/UseCases.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/VerificationCases.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/library/systems/Views.sysml +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/__init__.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/__main__.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/protocol.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/lsp/stdio.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/navigate.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/plantuml.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/project.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/semantic.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/sim.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/spreadsheet.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/store.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/traceability.py +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/us_customary_units.txt +0 -0
- {sysmlpy-0.81.0 → sysmlpy-0.83.0}/src/sysmlpy/validator.py +0 -0
|
@@ -34,7 +34,7 @@ __all__ = [
|
|
|
34
34
|
"render_state_transition_view_svg", "boxes_view",
|
|
35
35
|
]
|
|
36
36
|
__author__ = "Jon Fox"
|
|
37
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.83.0"
|
|
38
38
|
|
|
39
39
|
from sysmlpy.usage import (
|
|
40
40
|
Item, Attribute, Part, Port, Action, Reference, UseCase, Requirement, Interface, Message,
|
|
@@ -449,7 +449,7 @@ def __getattr__(name):
|
|
|
449
449
|
globals()[name] = fn
|
|
450
450
|
return fn
|
|
451
451
|
if name in ("ModelDiff", "ElementChange", "FieldChange",
|
|
452
|
-
"diff_models", "diff_files"):
|
|
452
|
+
"diff_models", "diff_files", "diff_state_machines"):
|
|
453
453
|
from sysmlpy import diff as _diff
|
|
454
454
|
fn = getattr(_diff, name)
|
|
455
455
|
globals()[name] = fn
|
|
@@ -117,15 +117,25 @@ def cmd_diff(args) -> int:
|
|
|
117
117
|
print(f"Parse error: {e}", file=sys.stderr)
|
|
118
118
|
return 2
|
|
119
119
|
|
|
120
|
+
threshold = getattr(args, "threshold", None)
|
|
121
|
+
if threshold is not None and not 0 <= threshold <= 1:
|
|
122
|
+
print(f"Error: --threshold expects a fraction in 0..1, "
|
|
123
|
+
f"got {threshold}", file=sys.stderr)
|
|
124
|
+
return 2
|
|
125
|
+
|
|
120
126
|
if args.format == "json":
|
|
121
127
|
import json as _json
|
|
122
128
|
print(_json.dumps({
|
|
123
129
|
"summary": d.summary(),
|
|
130
|
+
"elements_old": d.elements_old,
|
|
131
|
+
"elements_new": d.elements_new,
|
|
132
|
+
"change_rate": round(d.change_rate, 6),
|
|
124
133
|
"changes": [
|
|
125
134
|
{
|
|
126
135
|
"change": c.change,
|
|
127
136
|
"kind": c.kind,
|
|
128
137
|
"qualified_name": c.qualified_name,
|
|
138
|
+
"old_qualified_name": c.old_qualified_name,
|
|
129
139
|
"fields": [
|
|
130
140
|
{"field": fc.field, "old": fc.old,
|
|
131
141
|
"new": fc.new} for fc in c.fields
|
|
@@ -139,6 +149,14 @@ def cmd_diff(args) -> int:
|
|
|
139
149
|
else:
|
|
140
150
|
print(d.as_text(), end="")
|
|
141
151
|
|
|
152
|
+
if threshold is not None:
|
|
153
|
+
rate = d.change_rate
|
|
154
|
+
failed = rate > threshold
|
|
155
|
+
print(f"change gate: {len(d.changes)} changes / "
|
|
156
|
+
f"{d.elements_old} elements = {rate:.2%}; threshold "
|
|
157
|
+
f"{threshold:.2%} -> {'FAIL' if failed else 'OK'}")
|
|
158
|
+
return 1 if failed else 0
|
|
159
|
+
|
|
142
160
|
# CI-friendly: differences are findings
|
|
143
161
|
return 1 if not d.is_empty() else 0
|
|
144
162
|
|
|
@@ -762,6 +780,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
762
780
|
"--format", choices=("text", "markdown", "json"), default="text",
|
|
763
781
|
help="Output format: text (default), markdown or JSON",
|
|
764
782
|
)
|
|
783
|
+
p_diff.add_argument(
|
|
784
|
+
"--threshold", type=float, default=None, metavar="FRAC",
|
|
785
|
+
help="CI gate: exit 1 only when the change rate (changes / old "
|
|
786
|
+
"elements) exceeds FRAC (0..1); exit 0 under the gate "
|
|
787
|
+
"even when the models differ. Without this flag any "
|
|
788
|
+
"difference exits 1.",
|
|
789
|
+
)
|
|
765
790
|
p_diff.set_defaults(func=cmd_diff)
|
|
766
791
|
|
|
767
792
|
# -- view ----------------------------------------------------------------
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"""Semantic model diff for review workflows (Goal 8 + Goal 11 Batch 3).
|
|
2
|
+
|
|
3
|
+
Compares two loaded models element-by-element and reports what
|
|
4
|
+
changed, so review workflows (PR gates, design comparisons) can see
|
|
5
|
+
*semantic* changes rather than whitespace/formatting noise.
|
|
6
|
+
|
|
7
|
+
Element identity is ``(kind, qualified name)`` — ``Fleet::Vehicle``
|
|
8
|
+
as a ``PartDef`` is a different identity from a ``PartUsage`` with
|
|
9
|
+
the same name. Every element carries a *signature* — the semantic
|
|
10
|
+
attributes the library tracks reliably:
|
|
11
|
+
|
|
12
|
+
- ``typing`` — the declared type name (``typed_by_name``, qualified)
|
|
13
|
+
- ``subject`` — requirement subjects (``(name, type)`` tuples)
|
|
14
|
+
- ``doc`` — documentation text (tree attribute; populated for
|
|
15
|
+
requirement-style kinds)
|
|
16
|
+
- ``value`` — the default-value expression (``= 70``), from the
|
|
17
|
+
element's dump text
|
|
18
|
+
- ``multiplicity`` — ``[2]``, ``[1..3] ordered nonunique`` etc.
|
|
19
|
+
- ``direction`` — ``in`` / ``out`` / ``inout`` feature direction
|
|
20
|
+
- ``abstract`` — ``abstract`` flag on definitions
|
|
21
|
+
- ``traces`` — requirement trace edges (``satisfy:x``, ``verify:y``)
|
|
22
|
+
via :func:`sysmlpy.traceability.extract_traceability`
|
|
23
|
+
|
|
24
|
+
Value / multiplicity / direction / abstract are *heuristic* reads of
|
|
25
|
+
the element's canonical ``dump()`` text — the dump is the library's
|
|
26
|
+
own canonical serialization, so both sides of the diff extract the
|
|
27
|
+
same way. The value regex does not span strings containing ``;``.
|
|
28
|
+
Documentation is *not* extracted from dumps — ``doc /* ... */``
|
|
29
|
+
members do not survive the grammar round-trip on usage/definition
|
|
30
|
+
kinds — so the ``doc`` field reflects the tree attribute (populated
|
|
31
|
+
for requirements).
|
|
32
|
+
|
|
33
|
+
Change kinds:
|
|
34
|
+
|
|
35
|
+
- ``added`` — element exists only in the new model
|
|
36
|
+
- ``removed`` — element exists only in the old model
|
|
37
|
+
- ``renamed`` — a removed/added pair matched by *rename detection*:
|
|
38
|
+
same kind and same structural signature (everything except
|
|
39
|
+
``doc``, which often changes alongside a rename) with exactly one
|
|
40
|
+
candidate. Ambiguous matches (several candidates) stay
|
|
41
|
+
removed + added. ``old_name``/``old_qualified_name`` carry the
|
|
42
|
+
previous identity; ``fields`` lists any signature drift.
|
|
43
|
+
- ``changed`` — same identity, different signature; ``fields`` lists
|
|
44
|
+
the field-level old/new values
|
|
45
|
+
|
|
46
|
+
State machines: :func:`diff_state_machines` diffs two models' state
|
|
47
|
+
machines through the simulator's :class:`~sysmlpy.sim.MachineDescriptor`
|
|
48
|
+
— states, transitions (source/target/trigger/guard/effect/history)
|
|
49
|
+
and the initial state.
|
|
50
|
+
|
|
51
|
+
Excluded by design: ``Model`` objects carry a random UUID name per
|
|
52
|
+
parse, so they are never compared.
|
|
53
|
+
|
|
54
|
+
Usage::
|
|
55
|
+
|
|
56
|
+
from sysmlpy.diff import diff_models, diff_files, diff_state_machines
|
|
57
|
+
|
|
58
|
+
d = diff_models(old_model, new_model)
|
|
59
|
+
print(d.as_text())
|
|
60
|
+
d2 = diff_files("old.sysml", "new.sysml")
|
|
61
|
+
md = diff_state_machines(old_model, new_model, focus="Cruise")
|
|
62
|
+
|
|
63
|
+
CLI: ``sysmlpy diff OLD NEW [--format text|markdown|json]
|
|
64
|
+
[--threshold 0.1]`` — with ``--threshold``, exit 1 only when the
|
|
65
|
+
change rate (changes / old elements) exceeds the gate.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
from __future__ import annotations
|
|
69
|
+
|
|
70
|
+
import re
|
|
71
|
+
from dataclasses import dataclass, field
|
|
72
|
+
from pathlib import Path
|
|
73
|
+
from typing import Any
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"FieldChange",
|
|
77
|
+
"ElementChange",
|
|
78
|
+
"ModelDiff",
|
|
79
|
+
"diff_models",
|
|
80
|
+
"diff_files",
|
|
81
|
+
"diff_state_machines",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# data types
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class FieldChange:
|
|
92
|
+
"""One changed field within an element."""
|
|
93
|
+
|
|
94
|
+
field: str
|
|
95
|
+
old: str | None
|
|
96
|
+
new: str | None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class ElementChange:
|
|
101
|
+
"""One element-level semantic change."""
|
|
102
|
+
|
|
103
|
+
change: str # "added" | "removed" | "renamed" | "changed"
|
|
104
|
+
kind: str # e.g. "PartDef", "PartUsage", "Package", "Transition"
|
|
105
|
+
name: str
|
|
106
|
+
qualified_name: str
|
|
107
|
+
fields: list = field(default_factory=list) # list[FieldChange]
|
|
108
|
+
#: previous name/qname — populated for ``renamed`` entries
|
|
109
|
+
old_name: str | None = None
|
|
110
|
+
old_qualified_name: str | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class ModelDiff:
|
|
115
|
+
"""The semantic diff between two models."""
|
|
116
|
+
|
|
117
|
+
changes: list = field(default_factory=list) # list[ElementChange]
|
|
118
|
+
#: number of comparable elements in the old/new models (for the
|
|
119
|
+
#: ``--threshold`` change-rate gate)
|
|
120
|
+
elements_old: int = 0
|
|
121
|
+
elements_new: int = 0
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
# queries
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def added(self) -> list:
|
|
129
|
+
return [c for c in self.changes if c.change == "added"]
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def removed(self) -> list:
|
|
133
|
+
return [c for c in self.changes if c.change == "removed"]
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def renamed(self) -> list:
|
|
137
|
+
return [c for c in self.changes if c.change == "renamed"]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def changed(self) -> list:
|
|
141
|
+
return [c for c in self.changes if c.change == "changed"]
|
|
142
|
+
|
|
143
|
+
def is_empty(self) -> bool:
|
|
144
|
+
"""True when the models are semantically identical."""
|
|
145
|
+
return not self.changes
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def change_rate(self) -> float:
|
|
149
|
+
"""Fraction of the old model affected by this diff.
|
|
150
|
+
|
|
151
|
+
``changes / elements_old`` (0.0 when the old model is empty).
|
|
152
|
+
Used by the ``--threshold`` CI gate.
|
|
153
|
+
"""
|
|
154
|
+
if not self.elements_old:
|
|
155
|
+
return 0.0 if not self.changes else 1.0
|
|
156
|
+
return len(self.changes) / self.elements_old
|
|
157
|
+
|
|
158
|
+
def summary(self) -> str:
|
|
159
|
+
"""One-line human summary, e.g. ``2 added, 1 removed, 3 changed``."""
|
|
160
|
+
if self.is_empty():
|
|
161
|
+
return "models are semantically identical"
|
|
162
|
+
counts = [f"{len(self.added)} added",
|
|
163
|
+
f"{len(self.removed)} removed",
|
|
164
|
+
f"{len(self.renamed)} renamed",
|
|
165
|
+
f"{len(self.changed)} changed"]
|
|
166
|
+
return ", ".join(counts)
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
# rendering
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def as_text(self) -> str:
|
|
173
|
+
"""Plain-text rendering (monochrome — review-log friendly)."""
|
|
174
|
+
if self.is_empty():
|
|
175
|
+
return "models are semantically identical\n"
|
|
176
|
+
marker = {"added": "+", "removed": "-", "renamed": ">",
|
|
177
|
+
"changed": "~"}
|
|
178
|
+
out = [f"Model diff: {self.summary()}", ""]
|
|
179
|
+
for c in self.changes:
|
|
180
|
+
if c.change == "renamed":
|
|
181
|
+
out.append(f"> {c.kind} {c.old_qualified_name}"
|
|
182
|
+
f" -> {c.qualified_name}")
|
|
183
|
+
else:
|
|
184
|
+
out.append(f"{marker[c.change]} {c.kind} "
|
|
185
|
+
f"{c.qualified_name}")
|
|
186
|
+
for fc in c.fields:
|
|
187
|
+
old_v = fc.old if fc.old is not None else "(none)"
|
|
188
|
+
new_v = fc.new if fc.new is not None else "(none)"
|
|
189
|
+
out.append(f" {fc.field}: {old_v} -> {new_v}")
|
|
190
|
+
return "\n".join(out) + "\n"
|
|
191
|
+
|
|
192
|
+
def as_markdown(self) -> str:
|
|
193
|
+
"""Markdown rendering for review workflows (PR comments etc.)."""
|
|
194
|
+
if self.is_empty():
|
|
195
|
+
return "Models are semantically identical.\n"
|
|
196
|
+
out = [f"## Model diff", "",
|
|
197
|
+
f"**{self.summary()}**", ""]
|
|
198
|
+
sections = [
|
|
199
|
+
("added", "### Added"),
|
|
200
|
+
("removed", "### Removed"),
|
|
201
|
+
("renamed", "### Renamed"),
|
|
202
|
+
("changed", "### Changed"),
|
|
203
|
+
]
|
|
204
|
+
for kind, header in sections:
|
|
205
|
+
items = [c for c in self.changes if c.change == kind]
|
|
206
|
+
if not items:
|
|
207
|
+
continue
|
|
208
|
+
out.append(header)
|
|
209
|
+
out.append("")
|
|
210
|
+
for c in items:
|
|
211
|
+
if kind == "renamed":
|
|
212
|
+
out.append(f"- `{c.old_qualified_name}` → "
|
|
213
|
+
f"`{c.qualified_name}` ({c.kind})")
|
|
214
|
+
else:
|
|
215
|
+
out.append(f"- `{c.qualified_name}` ({c.kind})")
|
|
216
|
+
for fc in c.fields:
|
|
217
|
+
old_v = fc.old if fc.old is not None else "—"
|
|
218
|
+
new_v = fc.new if fc.new is not None else "—"
|
|
219
|
+
out.append(
|
|
220
|
+
f" - `{fc.field}`: `{old_v}` → `{new_v}`")
|
|
221
|
+
out.append("")
|
|
222
|
+
return "\n".join(out).rstrip() + "\n"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# signature extraction
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
_FIELDS = ("typing", "subject", "doc", "value", "multiplicity",
|
|
230
|
+
"direction", "abstract", "traces")
|
|
231
|
+
#: fields used to match renames (``doc`` often changes with the name)
|
|
232
|
+
_STRUCTURAL = tuple(f for f in _FIELDS if f != "doc")
|
|
233
|
+
|
|
234
|
+
_DIR_RE = re.compile(r"^(?:in|out|inout)\b")
|
|
235
|
+
_MULT_RE = re.compile(r"\[([^\]]+)\]")
|
|
236
|
+
_VALUE_RE = re.compile(r"(?<![<>=!])=\s*([^;]+);\s*$")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _kind(obj) -> str:
|
|
240
|
+
"""Element kind with a definition/usage suffix.
|
|
241
|
+
|
|
242
|
+
``PartDef`` / ``PartUsage`` — the same qualified name under both
|
|
243
|
+
roles is a different identity, so e.g. turning ``part def X`` into
|
|
244
|
+
an inline ``part x : X`` reports as removed + added, not changed.
|
|
245
|
+
"""
|
|
246
|
+
base = type(obj).__name__
|
|
247
|
+
return base + ("Def" if getattr(obj, "is_definition", False)
|
|
248
|
+
else "Usage")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _dump_fields(obj) -> dict:
|
|
252
|
+
"""Heuristic signature fields read from the element's canonical dump.
|
|
253
|
+
|
|
254
|
+
The dump is the library's own canonical serialization, so the same
|
|
255
|
+
element dumps identically on both sides of a diff. Only the first
|
|
256
|
+
line is inspected for direction / multiplicity / value (usages are
|
|
257
|
+
single-line); the first ``doc /* ... */`` comment anywhere is the
|
|
258
|
+
element's own documentation in the common declaration order.
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
text = obj.dump()
|
|
262
|
+
except Exception:
|
|
263
|
+
return {}
|
|
264
|
+
if not text or not text.strip():
|
|
265
|
+
return {}
|
|
266
|
+
first = text.strip().splitlines()[0].strip()
|
|
267
|
+
out = {}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
m = _DIR_RE.match(first)
|
|
271
|
+
out["direction"] = m.group(0) if m else None
|
|
272
|
+
|
|
273
|
+
m = _MULT_RE.search(first)
|
|
274
|
+
if m:
|
|
275
|
+
flags = [f for f in ("ordered", "nonunique")
|
|
276
|
+
if re.search(rf"\b{f}\b", first)]
|
|
277
|
+
out["multiplicity"] = f"[{m.group(1)}]" + (
|
|
278
|
+
f" {' '.join(flags)}" if flags else "")
|
|
279
|
+
else:
|
|
280
|
+
out["multiplicity"] = None
|
|
281
|
+
|
|
282
|
+
m = _VALUE_RE.search(first)
|
|
283
|
+
out["value"] = m.group(1).strip() if m else None
|
|
284
|
+
|
|
285
|
+
out["abstract"] = "abstract" if re.match(r"^abstract\b", first) else None
|
|
286
|
+
|
|
287
|
+
return out
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _signature(obj, traces: dict | None = None) -> dict:
|
|
291
|
+
"""The comparable semantic attributes of a tree object."""
|
|
292
|
+
sig = {}
|
|
293
|
+
|
|
294
|
+
typing = getattr(obj, "typed_by_name", None)
|
|
295
|
+
sig["typing"] = str(typing) if typing else None
|
|
296
|
+
|
|
297
|
+
subject = getattr(obj, "subject", None)
|
|
298
|
+
if subject:
|
|
299
|
+
name, type_name = subject
|
|
300
|
+
sig["subject"] = f"{name} : {type_name}" if type_name else str(name)
|
|
301
|
+
else:
|
|
302
|
+
sig["subject"] = None
|
|
303
|
+
|
|
304
|
+
for f, v in _dump_fields(obj).items():
|
|
305
|
+
sig[f] = v
|
|
306
|
+
|
|
307
|
+
doc = getattr(obj, "doc", None)
|
|
308
|
+
if doc:
|
|
309
|
+
sig["doc"] = str(doc)
|
|
310
|
+
|
|
311
|
+
sig["traces"] = None
|
|
312
|
+
if traces:
|
|
313
|
+
from sysmlpy.traceability import _qualified_name
|
|
314
|
+
trace = traces.get(_qualified_name(obj))
|
|
315
|
+
if trace is not None:
|
|
316
|
+
edges = ([f"satisfy:{x}" for x in trace.satisfied_by] +
|
|
317
|
+
[f"verify:{x}" for x in trace.verified_by])
|
|
318
|
+
if edges:
|
|
319
|
+
sig["traces"] = ", ".join(sorted(edges))
|
|
320
|
+
|
|
321
|
+
return sig
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _collect(model, traces: dict | None = None) -> dict:
|
|
325
|
+
"""Map ``(kind, qualified name)`` -> ``(name, signature)``."""
|
|
326
|
+
from sysmlpy.traceability import _walk, _qualified_name
|
|
327
|
+
|
|
328
|
+
out = {}
|
|
329
|
+
for obj in _walk(model):
|
|
330
|
+
if type(obj).__name__ == "Model":
|
|
331
|
+
continue # random UUID name — not comparable
|
|
332
|
+
qname = _qualified_name(obj)
|
|
333
|
+
if not qname:
|
|
334
|
+
continue
|
|
335
|
+
key = (_kind(obj), qname)
|
|
336
|
+
out[key] = (getattr(obj, "name", ""), _signature(obj, traces))
|
|
337
|
+
return out
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
# public API
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def diff_models(old_model: Any, new_model: Any) -> ModelDiff:
|
|
346
|
+
"""Diff two loaded models semantically.
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
old_model, new_model : Model
|
|
351
|
+
Two models loaded via :func:`sysmlpy.loads` /
|
|
352
|
+
:func:`sysmlpy.load_files` (or any Model tree).
|
|
353
|
+
"""
|
|
354
|
+
from sysmlpy.traceability import extract_traceability
|
|
355
|
+
|
|
356
|
+
old_traces = new_traces = None
|
|
357
|
+
try:
|
|
358
|
+
old_traces = {t.qualified_name: t
|
|
359
|
+
for t in extract_traceability(old_model).requirements}
|
|
360
|
+
new_traces = {t.qualified_name: t
|
|
361
|
+
for t in extract_traceability(new_model).requirements}
|
|
362
|
+
except Exception:
|
|
363
|
+
old_traces = new_traces = None # traces are best-effort
|
|
364
|
+
|
|
365
|
+
old = _collect(old_model, old_traces)
|
|
366
|
+
new = _collect(new_model, new_traces)
|
|
367
|
+
|
|
368
|
+
# Rename detection: a removed element matches an added one when
|
|
369
|
+
# kind AND structural signature are equal and the candidate is
|
|
370
|
+
# unique — otherwise the pair stays removed + added.
|
|
371
|
+
removed_keys = sorted(k for k in old if k not in new)
|
|
372
|
+
added_keys = sorted(k for k in new if k not in old)
|
|
373
|
+
matches: dict = {}
|
|
374
|
+
used: set = set()
|
|
375
|
+
for rk in removed_keys:
|
|
376
|
+
sig_old = tuple(old[rk][1].get(f) for f in _STRUCTURAL)
|
|
377
|
+
candidates = [
|
|
378
|
+
ak for ak in added_keys
|
|
379
|
+
if ak not in used and ak[0] == rk[0]
|
|
380
|
+
and tuple(new[ak][1].get(f) for f in _STRUCTURAL) == sig_old
|
|
381
|
+
]
|
|
382
|
+
if len(candidates) == 1:
|
|
383
|
+
matches[rk] = candidates[0]
|
|
384
|
+
used.add(candidates[0])
|
|
385
|
+
|
|
386
|
+
matched_removed = set(matches)
|
|
387
|
+
matched_added = set(matches.values())
|
|
388
|
+
|
|
389
|
+
changes = []
|
|
390
|
+
for key in sorted((set(old) | set(new))
|
|
391
|
+
- matched_removed - matched_added):
|
|
392
|
+
in_old = key in old
|
|
393
|
+
in_new = key in new
|
|
394
|
+
kind, qname = key
|
|
395
|
+
if in_old and not in_new:
|
|
396
|
+
changes.append(ElementChange(
|
|
397
|
+
change="removed", kind=kind,
|
|
398
|
+
name=old[key][0], qualified_name=qname))
|
|
399
|
+
elif in_new and not in_old:
|
|
400
|
+
changes.append(ElementChange(
|
|
401
|
+
change="added", kind=kind,
|
|
402
|
+
name=new[key][0], qualified_name=qname))
|
|
403
|
+
else:
|
|
404
|
+
old_sig = old[key][1]
|
|
405
|
+
new_sig = new[key][1]
|
|
406
|
+
field_changes = [
|
|
407
|
+
FieldChange(field=f, old=old_sig.get(f),
|
|
408
|
+
new=new_sig.get(f))
|
|
409
|
+
for f in _FIELDS
|
|
410
|
+
if old_sig.get(f) != new_sig.get(f)
|
|
411
|
+
]
|
|
412
|
+
if field_changes:
|
|
413
|
+
changes.append(ElementChange(
|
|
414
|
+
change="changed", kind=kind,
|
|
415
|
+
name=new[key][0], qualified_name=qname,
|
|
416
|
+
fields=field_changes))
|
|
417
|
+
for rk in sorted(matched_removed):
|
|
418
|
+
ak = matches[rk]
|
|
419
|
+
kind = rk[0]
|
|
420
|
+
old_sig = old[rk][1]
|
|
421
|
+
new_sig = new[ak][1]
|
|
422
|
+
field_changes = [
|
|
423
|
+
FieldChange(field=f, old=old_sig.get(f), new=new_sig.get(f))
|
|
424
|
+
for f in _FIELDS if old_sig.get(f) != new_sig.get(f)
|
|
425
|
+
]
|
|
426
|
+
changes.append(ElementChange(
|
|
427
|
+
change="renamed", kind=kind,
|
|
428
|
+
name=new[ak][0], qualified_name=ak[1],
|
|
429
|
+
old_name=old[rk][0], old_qualified_name=rk[1],
|
|
430
|
+
fields=field_changes))
|
|
431
|
+
|
|
432
|
+
return ModelDiff(changes=changes, elements_old=len(old),
|
|
433
|
+
elements_new=len(new))
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def diff_files(old_path, new_path) -> ModelDiff:
|
|
437
|
+
"""Diff two SysML source files.
|
|
438
|
+
|
|
439
|
+
Parameters
|
|
440
|
+
----------
|
|
441
|
+
old_path, new_path : str | Path
|
|
442
|
+
Paths to ``.sysml`` files.
|
|
443
|
+
"""
|
|
444
|
+
import sysmlpy
|
|
445
|
+
|
|
446
|
+
old_model = sysmlpy.loads(Path(old_path).read_text(encoding="utf-8"))
|
|
447
|
+
new_model = sysmlpy.loads(Path(new_path).read_text(encoding="utf-8"))
|
|
448
|
+
return diff_models(old_model, new_model)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# ---------------------------------------------------------------------------
|
|
452
|
+
# state-machine diff (Goal 11 Batch 3)
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
_TRANSITION_FIELDS = ("source", "target", "trigger", "guard", "effect",
|
|
456
|
+
"history_region")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _transition_keys(descriptor) -> dict:
|
|
460
|
+
"""Identity for each transition (declaration order).
|
|
461
|
+
|
|
462
|
+
Named transitions use their name; anonymous ones use
|
|
463
|
+
``transition@<source>-><target>``. Duplicates get a ``#2``-style
|
|
464
|
+
suffix so every identity is unique.
|
|
465
|
+
"""
|
|
466
|
+
keys = {}
|
|
467
|
+
seen: dict = {}
|
|
468
|
+
for i, t in enumerate(descriptor.transitions):
|
|
469
|
+
base = t.name or f"transition@{t.source}->{t.target}"
|
|
470
|
+
n = seen.get(base, 0)
|
|
471
|
+
seen[base] = n + 1
|
|
472
|
+
keys[i] = base if n == 0 else f"{base}#{n + 1}"
|
|
473
|
+
return keys
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def diff_state_machines(old_model: Any, new_model: Any,
|
|
477
|
+
focus: str | None = None) -> ModelDiff:
|
|
478
|
+
"""Diff the state machines of two models.
|
|
479
|
+
|
|
480
|
+
Compares the machines through the simulator's extraction
|
|
481
|
+
(:func:`sysmlpy.sim.build_state_machine`): states, transitions
|
|
482
|
+
(source/target/trigger/guard/effect/history) and the initial
|
|
483
|
+
state. Transitions are identified by declaration name (anonymous
|
|
484
|
+
ones by their endpoints).
|
|
485
|
+
|
|
486
|
+
Raises :class:`sysmlpy.sim.SimulationError` when either model has
|
|
487
|
+
no (matching) state machine or declares parallel regions.
|
|
488
|
+
"""
|
|
489
|
+
from sysmlpy.sim import build_state_machine
|
|
490
|
+
|
|
491
|
+
old_md = build_state_machine(old_model, focus=focus)
|
|
492
|
+
new_md = build_state_machine(new_model, focus=focus)
|
|
493
|
+
|
|
494
|
+
changes = []
|
|
495
|
+
machine_name = new_md.name or old_md.name or ""
|
|
496
|
+
if old_md.initial != new_md.initial:
|
|
497
|
+
changes.append(ElementChange(
|
|
498
|
+
change="changed", kind="StateMachine", name=machine_name,
|
|
499
|
+
qualified_name=machine_name,
|
|
500
|
+
fields=[FieldChange(field="initial", old=old_md.initial,
|
|
501
|
+
new=new_md.initial)]))
|
|
502
|
+
|
|
503
|
+
old_states = set(old_md.states)
|
|
504
|
+
new_states = set(new_md.states)
|
|
505
|
+
for s in sorted(old_states - new_states):
|
|
506
|
+
changes.append(ElementChange(change="removed", kind="State",
|
|
507
|
+
name=s, qualified_name=s))
|
|
508
|
+
for s in sorted(new_states - old_states):
|
|
509
|
+
changes.append(ElementChange(change="added", kind="State",
|
|
510
|
+
name=s, qualified_name=s))
|
|
511
|
+
|
|
512
|
+
old_keys = _transition_keys(old_md)
|
|
513
|
+
new_keys = _transition_keys(new_md)
|
|
514
|
+
old_t = {old_keys[i]: t for i, t in enumerate(old_md.transitions)}
|
|
515
|
+
new_t = {new_keys[i]: t for i, t in enumerate(new_md.transitions)}
|
|
516
|
+
|
|
517
|
+
for key in sorted(set(old_t) | set(new_t)):
|
|
518
|
+
in_old, in_new = key in old_t, key in new_t
|
|
519
|
+
if in_old and not in_new:
|
|
520
|
+
changes.append(ElementChange(
|
|
521
|
+
change="removed", kind="Transition", name=key,
|
|
522
|
+
qualified_name=key))
|
|
523
|
+
elif in_new and not in_old:
|
|
524
|
+
changes.append(ElementChange(
|
|
525
|
+
change="added", kind="Transition", name=key,
|
|
526
|
+
qualified_name=key))
|
|
527
|
+
else:
|
|
528
|
+
o, n = old_t[key], new_t[key]
|
|
529
|
+
field_changes = [
|
|
530
|
+
FieldChange(field=f, old=getattr(o, f),
|
|
531
|
+
new=getattr(n, f))
|
|
532
|
+
for f in _TRANSITION_FIELDS
|
|
533
|
+
if getattr(o, f) != getattr(n, f)
|
|
534
|
+
]
|
|
535
|
+
if field_changes:
|
|
536
|
+
changes.append(ElementChange(
|
|
537
|
+
change="changed", kind="Transition", name=key,
|
|
538
|
+
qualified_name=key, fields=field_changes))
|
|
539
|
+
|
|
540
|
+
return ModelDiff(changes=changes, elements_old=len(old_md.states),
|
|
541
|
+
elements_new=len(new_md.states))
|