-
Notifications
You must be signed in to change notification settings - Fork 17
feat: added support for the libraries linking and referencing from model #619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0834f55
95876e0
642dc10
689caf9
afa4254
1c909f0
48f4663
e16380a
09d5b1b
29ca09b
a24c1b9
bf61bcc
c31203a
7a47cb1
0ffe456
51ff19d
35447dc
ba96398
cb6ed11
f7050f5
a6a3312
70b3180
6af3dc1
ee38b5c
9ac0245
6f0ae2c
af6deb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -80,6 +80,11 @@ | |||||||||||||||||||||||
| METADATA_TAG = f"{{{_n.NAMESPACES['metadata']}}}Metadata" | ||||||||||||||||||||||||
| _ROOT_NS = "org.polarsys.capella.core.data.capellamodeller" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| XP_SEMANTIC_RESOURCES = etree.XPath( | ||||||||||||||||||||||||
| "/viewpoint:DAnalysis/semanticResources", | ||||||||||||||||||||||||
| namespaces=_n.NAMESPACES, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _derive_entrypoint( | ||||||||||||||||||||||||
| path: str | os.PathLike | filehandler.FileHandler, | ||||||||||||||||||||||||
|
|
@@ -590,6 +595,106 @@ def check_duplicate_uuids(self) -> None: | |||||||||||||||||||||||
| " - check the 'resources' for duplicate models" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def __get_metadata(self, afm: ModelFile) -> etree._Element: | ||||||||||||||||||||||||
| """Return metadata from given model. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Parameters | ||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||
| afm | ||||||||||||||||||||||||
| model metadata file | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Returns | ||||||||||||||||||||||||
| ------- | ||||||||||||||||||||||||
| etree._Element | ||||||||||||||||||||||||
| metadata element if found | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Raises | ||||||||||||||||||||||||
| ------ | ||||||||||||||||||||||||
| RuntimeError | ||||||||||||||||||||||||
| If metadata could not be found | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| metadata = next(afm.root.iter(METADATA_TAG), None) | ||||||||||||||||||||||||
| if metadata is None: | ||||||||||||||||||||||||
| raise RuntimeError("Cannot find <Metadata> in primary .afm file") | ||||||||||||||||||||||||
| LOGGER.debug("Found <Metadata> with ID %s", metadata.get("id")) | ||||||||||||||||||||||||
| return metadata | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def link_library(self, lib: pathlib.PurePosixPath) -> None: | ||||||||||||||||||||||||
| """Link library into the project tree. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Parameters | ||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||
| lib | ||||||||||||||||||||||||
| path to a library .aird file | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Description | ||||||||||||||||||||||||
| ----------- | ||||||||||||||||||||||||
| Method updates .aird, .capella, .afm to correcrly reflect library in target model | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| When you need to refere to external library (or reuse one in a project) | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
| p = "..." #path to library | ||||||||||||||||||||||||
| model._loader.link_library(p) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| lib = model.project.extensions[0].reference.library | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docs build is failing because sphinx doesn't like this docstring in particular.
You can test the docs build locally as well (which should be much faster than waiting for CI) with a simple |
||||||||||||||||||||||||
| handler = self.resources[str(lib)] | ||||||||||||||||||||||||
| _h, filename = _derive_entrypoint(handler) | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| frag_path = lib.joinpath(filename) | ||||||||||||||||||||||||
| if self.trees.get(frag_path) is not None: | ||||||||||||||||||||||||
| # for already loaded fragments skip library loading | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| frag = ModelFile( | ||||||||||||||||||||||||
| filename, handler, ignore_uuid_dups=self.__ignore_uuid_dups | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
Wuestengecko marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| self.trees[frag_path] = frag | ||||||||||||||||||||||||
| for ref in _find_refs(frag.root): | ||||||||||||||||||||||||
| ref_name = helpers.normalize_pure_path( | ||||||||||||||||||||||||
| _unquote_ref(ref), base=frag_path.parent | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| self.__load_referenced_files(ref_name) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if ref_name.suffix == ".afm": | ||||||||||||||||||||||||
| meta_lib = self.__get_metadata(self.trees[ref_name]) | ||||||||||||||||||||||||
| meta_self = self.__find_metadata() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if not next( | ||||||||||||||||||||||||
| filter( | ||||||||||||||||||||||||
| lambda el: re.search(str(ref_name), el.attrib["href"]), | ||||||||||||||||||||||||
| meta_self.iterchildren("additionalResources"), | ||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
| None, | ||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||
|
Comment on lines
+665
to
+671
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're doing a simple substring match here, no need to use a regex. (Same applies a few lines further down for Also, the
Suggested change
|
||||||||||||||||||||||||
| ael = meta_self.makeelement( | ||||||||||||||||||||||||
| "additionalMetadata", | ||||||||||||||||||||||||
| href=f"../{ref_name}#{meta_lib.attrib['id']}", | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| meta_self.append(ael) | ||||||||||||||||||||||||
| elif ref_name.suffix == ".capella": | ||||||||||||||||||||||||
| aird_self = self.trees[ | ||||||||||||||||||||||||
| pathlib.PurePosixPath(f"\x00/{self.entrypoint}") | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| last = next( | ||||||||||||||||||||||||
| filter( | ||||||||||||||||||||||||
| lambda el: re.search(str(ref_name), el.text), | ||||||||||||||||||||||||
| XP_SEMANTIC_RESOURCES(aird_self.root), | ||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
| None, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
Comment on lines
+682
to
+688
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same points as above also apply here (regex for substring match and filter+lambda overhead). Also, here we're doing the XPath lookup twice, which I think we could combine into a single iteration like this (notice that the for el in XP_...:
if str(ref_name) in el.text:
break
else:
sr = el.makeelement(...) # etc. |
||||||||||||||||||||||||
| if last is not None: | ||||||||||||||||||||||||
| for r in XP_SEMANTIC_RESOURCES(aird_self.root): | ||||||||||||||||||||||||
| last = r | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if last is not None: | ||||||||||||||||||||||||
| sr = last.makeelement("semanticResources") | ||||||||||||||||||||||||
| sr.text = f"platform:/resource/{ref_name}" | ||||||||||||||||||||||||
| last.addnext(sr) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def __load_referenced_files( | ||||||||||||||||||||||||
| self, resource_path: pathlib.PurePosixPath | ||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||
|
|
@@ -1302,6 +1407,15 @@ def _find_fragment( | |||||||||||||||||||||||
| for fragment, tree in self.trees.items(): | ||||||||||||||||||||||||
| if tree.root is root: | ||||||||||||||||||||||||
| return (fragment, tree) | ||||||||||||||||||||||||
| # fallback here is necessary because | ||||||||||||||||||||||||
| # working with root collections, i.e. key_value_pairs, | ||||||||||||||||||||||||
| # causes project root being represented by two entities, i.e. | ||||||||||||||||||||||||
| # <Element {http://www.polarsys.org/capella/core/modeller/7.0.0}Project at 0x7feced455dc0> | ||||||||||||||||||||||||
| # <Element {http://www.polarsys.org/capella/core/modeller/7.0.0}Project at 0x7fecee0f6640> | ||||||||||||||||||||||||
| # they do not pass "is" check but have the same IDs | ||||||||||||||||||||||||
| for fragment, tree in self.trees.items(): | ||||||||||||||||||||||||
| if tree.root.attrib.get("id") == root.attrib.get("id"): | ||||||||||||||||||||||||
| return (fragment, tree) | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds odd. There should only ever be exactly one LXML entity per model element, especially the project root (under which everything else is nested).
All of these could easily lead to corrupted models and data loss, so this needs to be addressed before merging. If you can, could you add a test case which hits this case? |
||||||||||||||||||||||||
| raise ValueError("Element is not contained in any fragment") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _follow_href(self, element: etree._Element) -> etree._Element: | ||||||||||||||||||||||||
|
|
@@ -1345,11 +1459,7 @@ def __find_metadata(self) -> etree._Element: | |||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| if afm is None: | ||||||||||||||||||||||||
| raise RuntimeError("Cannot find .afm file in primary resource") | ||||||||||||||||||||||||
| metadata = next(afm.root.iter(METADATA_TAG), None) | ||||||||||||||||||||||||
| if metadata is None: | ||||||||||||||||||||||||
| raise RuntimeError("Cannot find <Metadata> in primary .afm file") | ||||||||||||||||||||||||
| LOGGER.debug("Found <Metadata> with ID %s", metadata.get("id")) | ||||||||||||||||||||||||
| return metadata | ||||||||||||||||||||||||
| return self.__get_metadata(afm) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def referenced_viewpoints(self) -> cabc.Iterator[tuple[str, str]]: | ||||||||||||||||||||||||
| metadata = self.__find_metadata() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| from __future__ import annotations | ||
|
|
||
| import capellambse.model as m | ||
| from capellambse.metamodel import re | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know why Github isn't running CI for this PR. Could you please make sure you run all the pre-commit hooks locally with |
||
|
|
||
| from . import capellacore | ||
| from . import namespaces as ns | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,9 +83,11 @@ class SequenceMessage(capellacore.NamedElement): | |
| exchange_context = m.Association["capellacore.Constraint"]( | ||
| (ns.CAPELLACORE, "Constraint"), "exchangeContext" | ||
| ) | ||
| sending_end = m.Association["MessageEnd"]((NS, "MessageEnd"), "sendingEnd") | ||
| receiving_end = m.Association["MessageEnd"]( | ||
| (NS, "MessageEnd"), "receivingEnd" | ||
| sending_end = m.Single( | ||
| m.Association["MessageEnd"]((NS, "MessageEnd"), "sendingEnd") | ||
| ) | ||
| receiving_end = m.Single( | ||
| m.Association["MessageEnd"]((NS, "MessageEnd"), "receivingEnd") | ||
| ) | ||
|
Comment on lines
+86
to
91
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move the bracketed type annotation up a level to the -m.Single(
- m.Association["Annotation"](...)
+m.Single["Annotation"](
+ m.Assocation(...)
)Same also in the other metamodel files you modified. |
||
| exchanged_items = m.Association["information.ExchangeItem"]( | ||
| (ns.INFORMATION, "ExchangeItem"), "exchangedItems" | ||
|
|
@@ -218,14 +220,13 @@ class _EventOperation(Event): | |
|
|
||
|
|
||
| class EventReceiptOperation(_EventOperation): | ||
| operation = m.Association["information.AbstractEventOperation"]( | ||
| (ns.INFORMATION, "AbstractEventOperation"), "operation" | ||
| operation = m.Single( | ||
| m.Association["information.AbstractEventOperation"]((ns.INFORMATION, "AbstractEventOperation"), "operation") | ||
| ) | ||
|
|
||
|
|
||
| class EventSentOperation(_EventOperation): | ||
| operation = m.Association["information.AbstractEventOperation"]( | ||
| (ns.INFORMATION, "AbstractEventOperation"), "operation" | ||
| operation = m.Single( | ||
| m.Association["information.AbstractEventOperation"]((ns.INFORMATION, "AbstractEventOperation"), "operation") | ||
| ) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -495,7 +495,6 @@ def __setitem__( | |
| it = self._parent.iterchildren(self._tag) | ||
| elems = list(itertools.islice(it, idx.start, idx.stop, idx.step)) | ||
| values = [i.text for i in elems] | ||
| values[idx] = value | ||
| for e, v in zip(elems, values, strict=True): | ||
| e.text = v | ||
|
Comment on lines
496
to
500
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, this is/was indeed bugged. But I think the fix is incomplete: Now the elems = itertools.islice(it, idx.start, idx.stop, idx.step)
for e, v in zip(elems, value, strict=True):
e.text = v |
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This exception message doesn't quite fit anymore, since this function is also used for auxiliary
.afmfiles. Let's simply show the filename instead: