Skip to content

feat: Add version_range property to Component for CycloneDX v1.7 - #1032

Open
nulano wants to merge 2 commits into
CycloneDX:mainfrom
nulano:feature/version-range
Open

feat: Add version_range property to Component for CycloneDX v1.7#1032
nulano wants to merge 2 commits into
CycloneDX:mainfrom
nulano:feature/version-range

Conversation

@nulano

@nulano nulano commented Aug 28, 2026

Copy link
Copy Markdown

Description

Implement the versionRange property for Compoments as specified in the CycloneDX v1.7 schema. The versionRange can be specified instead of the version for external components to specify the accepted version range.

  • Add version_range property to Component with serialization decorators.
  • Adjust Component.repr to show whether a component is external and the version_range if provided.
  • Extend Bom.validate with validation for CycloneDX v1.7 requirements regarding versionRange and isExternal.
  • Add unit tests for Component.version_range proprety and Bom.validate changes.
  • Add serialization snapshot tests for various kinds of external components.

Resolves or fixes issue: part of #903 (versionRange)

AI Tool Disclosure

  • My contribution does not include any AI-generated content
  • My contribution includes AI-generated content, as disclosed below:
    • AI Tools: [e.g. GitHub CoPilot, ChatGPT, JetBrains Junie etc.]
    • LLMs and versions: [e.g. GPT-4.1, Claude Haiku 4.5, Gemini 2.5 Pro etc.]
    • Prompts: [Summarize the key prompts or instructions given to the AI tools]

Affirmation

Implement the versionRange property for Compoments as specified in the CycloneDX v1.7 schema.
The versionRange can be specified instead of the version for external components to specify the accepted version range.

* Add version_range property to Component with serialization decorators.
* Adjust Component.__repr__ to show whether a component is external and the version_range if provided.
* Extend Bom.validate with validation for CycloneDX v1.7 requirements regarding versionRange and isExternal.
* Add unit tests for Component.version_range proprety and Bom.validate changes.
* Add serialization snapshot tests for various kinds of external components.

Implements part of CycloneDX#903

Signed-off-by: Ondrej Baranovič <nulano@nulano.eu>
@nulano
nulano requested a review from a team as a code owner August 28, 2026 15:55
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 duplication

Metric Results
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@read-the-docs-community

read-the-docs-community Bot commented Aug 28, 2026

Copy link
Copy Markdown

@CAOShurong CAOShurong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head c1b71ab5055b684aae1e160582dda1c10671ee80.

I checked the v1.7-only versionRange serialization, component equality/order behavior, root isExternal warning, and both invalid combinations (version plus versionRange, and versionRange without isExternal=true). The invalid model fixtures are picked up by the suite's reflective *_invalid discovery, so the new validation paths are exercised in both JSON and XML output tests.

Verification on this exact head:

  • full suite with PYTHONUTF8=1: 7,061 tests, pass
  • focused model/output suite: 834 tests, pass
  • mypy, flake8, compileall, and git diff --check: pass

The three failures without PYTHONUTF8=1 were existing Windows GBK decoding of UTF-8 fixture files, not failures in this change. I did not find a blocking correctness issue in the patch.

AI assistance disclosure: Codex helped run and cross-check the review; I verified the cited head, diff, tests, and conclusions.

Comment thread cyclonedx/model/component.py Outdated

@version_range.setter
def version_range(self, version_range: Optional[str]) -> None:
if version_range and not 1 <= len(version_range) <= 4096:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for version_range='' - when the len is <=1 - this would not print the appropriate warning
or does it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I knew I was gonna forget an is not None somewhere... fixed.

Comment thread cyclonedx/model/component.py Outdated
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.type, self.group, self.name, self.version,
self.type, self.group, self.name, self.version, self.version_range,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new properties shall go to the end of the list.

@nulano nulano Aug 31, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reordered

Comment thread cyclonedx/model/component.py Outdated
def __repr__(self) -> str:
return f'<Component bom-ref={self.bom_ref!r}, group={self.group}, name={self.name}, ' \
f'version={self.version}, type={self.type}>'
if not self.is_external:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too complex.

can this be shortened to something like

version_constraint = f'versionRange={self.versionRange}' \
  if self.versionRange \
  else f'version={self.version}'
return f'<Component bom-ref={self.bom_ref!r}, group={self.group}, name={self.name}, ' \
        ) \
       f'{versionRange}, type={self.type}>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied suggestion with minor changes

<component type="library" bom-ref="external-lib-1.0.0">
<name>external-lib</name>
<version>1.0.0</version>
<scope>required</scope>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where did this go?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It got left out while reorganizing code as it is not mandatory, but I've put it back now.

Comment thread tests/_data/models.py Outdated

def get_bom_with_external_component_1_7() -> Bom:
bom = _make_bom(components=[get_component_external()])
def get_bom_with_external_component_without_version() -> Bom:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd rather not have one file for each individual case,
just add all your component cases to the existing get_bom_with_external_component_1_7
and extend it.
Something like

def get_bom_with_external_component_1_7() -> Bom:
    bom = _make_bom(components=[
get_component_external_without_version(),
get_component_external_with_version(),
get_component_external_with_version_range()
])

def get_bom_with_external_component_invalid() -> Bom:
  bom = _make_bom(components=[
get_component_external_with_version_and_version_range_invalid(),
  ])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you mean one BOM with all valid components and separate BOMs for invalid components, otherwise the invalid tests wouldn't work right. Valid BOMs combined.

Comment thread tests/test_model_bom.py
self.assertEqual(len(w.warnings), 1)
self.assertIn('has no defined dependencies ', str(w.warnings[0]))

def test_warning_root_component_is_external(self) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread cyclonedx/model/bom.py
raise LicenseExpressionAlongWithOthersException(
f'Found LicenseExpression along with others licenses in: {elem!r}')

# 4. Validates that each component conforms to CycloneDX 1.7 constraints:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Signed-off-by: Ondrej Baranovič <nulano@nulano.eu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants