Skip to content

Commit 71edacf

Browse files
authored
feat: make schema deprecation warnings handle-able (#945)
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent 6460b71 commit 71edacf

5 files changed

Lines changed: 146 additions & 16 deletions

File tree

cyclonedx/model/bom.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .._internal.compare import ComparableTuple as _ComparableTuple
3131
from .._internal.time import get_now_utc as _get_now_utc
3232
from ..exception.model import LicenseExpressionAlongWithOthersException, UnknownComponentDependencyException
33+
from ..schema.deprecation import SchemaDeprecationWarning1Dot6
3334
from ..schema.schema import (
3435
SchemaVersion1Dot0,
3536
SchemaVersion1Dot1,
@@ -291,10 +292,7 @@ def manufacture(self, manufacture: Optional[OrganizationalEntity]) -> None:
291292
we should set this data on `.component.manufacturer`.
292293
"""
293294
if manufacture is not None:
294-
warn(
295-
'`bom.metadata.manufacture` is deprecated from CycloneDX v1.6 onwards. '
296-
'Please use `bom.metadata.component.manufacturer` instead.',
297-
DeprecationWarning)
295+
SchemaDeprecationWarning1Dot6._warn('bom.metadata.manufacture', 'bom.metadata.component.manufacturer')
298296
self._manufacture = manufacture
299297

300298
@property

cyclonedx/model/component.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
SerializationOfUnexpectedValueException,
4141
SerializationOfUnsupportedComponentTypeException,
4242
)
43+
from ..schema.deprecation import SchemaDeprecationWarning1Dot3, SchemaDeprecationWarning1Dot6
4344
from ..schema.schema import (
4445
SchemaVersion1Dot0,
4546
SchemaVersion1Dot1,
@@ -1185,8 +1186,7 @@ def author(self) -> Optional[str]:
11851186
@author.setter
11861187
def author(self, author: Optional[str]) -> None:
11871188
if author is not None:
1188-
warn('`@.author` is deprecated from CycloneDX v1.6 onwards. '
1189-
'Please use `@.authors` or `@.manufacturer` instead.', DeprecationWarning)
1189+
SchemaDeprecationWarning1Dot6._warn('@.author', '@.authors` or `@.manufacturer')
11901190
self._author = author
11911191

11921192
@property
@@ -1467,8 +1467,7 @@ def modified(self) -> bool:
14671467
@modified.setter
14681468
def modified(self, modified: bool) -> None:
14691469
if modified:
1470-
warn('`@.modified` is deprecated from CycloneDX v1.3 onwards. '
1471-
'Please use `@.pedigree` instead.', DeprecationWarning)
1470+
SchemaDeprecationWarning1Dot3._warn('@.modified', '@.pedigree')
14721471
self._modified = modified
14731472

14741473
@property

cyclonedx/model/tool.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from collections.abc import Iterable
2020
from itertools import chain
2121
from typing import TYPE_CHECKING, Any, Optional, Union
22-
from warnings import warn
2322
from xml.etree.ElementTree import Element # nosec B405
2423

2524
import py_serializable as serializable
@@ -28,6 +27,7 @@
2827

2928
from .._internal.compare import ComparableTuple as _ComparableTuple
3029
from ..schema import SchemaVersion
30+
from ..schema.deprecation import SchemaDeprecationWarning1Dot5
3131
from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6, SchemaVersion1Dot7
3232
from . import ExternalReference, HashType, _HashTypeRepositorySerializationHelper
3333
from .component import Component
@@ -240,9 +240,7 @@ def tools(self) -> 'SortedSet[Tool]':
240240
@tools.setter
241241
def tools(self, tools: Iterable[Tool]) -> None:
242242
if tools:
243-
warn('`@.tools` is deprecated from CycloneDX v1.5 onwards. '
244-
'Please use `@.components` and `@.services` instead.',
245-
DeprecationWarning)
243+
SchemaDeprecationWarning1Dot5._warn('@.tools', '@.components` and `@.services')
246244
self._tools = SortedSet(tools)
247245

248246
def __len__(self) -> int:

cyclonedx/schema/deprecation.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# This file is part of CycloneDX Python Library
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
# Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
19+
"""
20+
CycloneDX Schema Deprecation Warnings
21+
=====================================
22+
23+
This module provides warning classes for deprecated features in CycloneDX schemas.
24+
Each warning class corresponds to a specific schema version, enabling downstream
25+
code to catch, filter, or otherwise handle schema-specific deprecation warnings.
26+
27+
Intended Usage
28+
--------------
29+
30+
Downstream consumers can manage warnings using Python's ``warnings`` module.
31+
Common scenarios include:
32+
33+
- Filtering by schema version
34+
- Suppressing warnings in tests or batch processing
35+
- Logging or reporting deprecation warnings without raising exceptions
36+
37+
Example
38+
-------
39+
40+
.. code-block:: python
41+
42+
import warnings
43+
from cyclonedx.schema.deprecation import (
44+
BaseSchemaDeprecationWarning,
45+
SchemaDeprecationWarning1Dot7,
46+
)
47+
48+
# Suppress all CycloneDX schema deprecation warnings
49+
warnings.filterwarnings("ignore", category=BaseSchemaDeprecationWarning)
50+
51+
# Suppress only warnings specific to schema version 1.7
52+
warnings.filterwarnings("ignore", category=SchemaDeprecationWarning1Dot7)
53+
54+
Notes
55+
-----
56+
57+
- All deprecation warnings inherit from :class:`BaseSchemaDeprecationWarning`.
58+
- The ``SCHEMA_VERSION`` class variable indicates the CycloneDX schema version
59+
where the feature became deprecated.
60+
- These warning classes are designed for downstream **filtering and logging**,
61+
not for raising exceptions.
62+
"""
63+
64+
65+
from abc import ABC
66+
from typing import ClassVar, Literal, Optional
67+
from warnings import warn
68+
69+
from . import SchemaVersion
70+
71+
__all__ = [
72+
'BaseSchemaDeprecationWarning',
73+
'SchemaDeprecationWarning1Dot1',
74+
'SchemaDeprecationWarning1Dot2',
75+
'SchemaDeprecationWarning1Dot3',
76+
'SchemaDeprecationWarning1Dot4',
77+
'SchemaDeprecationWarning1Dot5',
78+
'SchemaDeprecationWarning1Dot6',
79+
'SchemaDeprecationWarning1Dot7',
80+
]
81+
82+
83+
class BaseSchemaDeprecationWarning(DeprecationWarning, ABC):
84+
"""Base class for warnings about deprecated schema features."""
85+
86+
SCHEMA_VERSION: ClassVar[SchemaVersion]
87+
88+
@classmethod
89+
def _warn(cls, deprecated: str, instead: Optional[str] = None, *, stacklevel: int = 1) -> None:
90+
"""Internal API. Not part of the public interface."""
91+
msg = f'`{deprecated}` is deprecated from CycloneDX v{cls.SCHEMA_VERSION.to_version()} onwards.'
92+
if instead:
93+
msg += f' Please use `{instead}` instead.'
94+
warn(msg, category=cls, stacklevel=stacklevel + 1)
95+
96+
97+
class SchemaDeprecationWarning1Dot7(BaseSchemaDeprecationWarning):
98+
"""Class for warnings about deprecated schema features in CycloneDX 1.7"""
99+
SCHEMA_VERSION: ClassVar[Literal[SchemaVersion.V1_7]] = SchemaVersion.V1_7
100+
101+
102+
class SchemaDeprecationWarning1Dot6(BaseSchemaDeprecationWarning):
103+
"""Class for warnings about deprecated schema features in CycloneDX 1.6"""
104+
SCHEMA_VERSION: ClassVar[Literal[SchemaVersion.V1_6]] = SchemaVersion.V1_6
105+
106+
107+
class SchemaDeprecationWarning1Dot5(BaseSchemaDeprecationWarning):
108+
"""Class for warnings about deprecated schema features in CycloneDX 1.5"""
109+
SCHEMA_VERSION: ClassVar[Literal[SchemaVersion.V1_5]] = SchemaVersion.V1_5
110+
111+
112+
class SchemaDeprecationWarning1Dot4(BaseSchemaDeprecationWarning):
113+
"""Class for warnings about deprecated schema features in CycloneDX 1.4"""
114+
SCHEMA_VERSION: ClassVar[Literal[SchemaVersion.V1_4]] = SchemaVersion.V1_4
115+
116+
117+
class SchemaDeprecationWarning1Dot3(BaseSchemaDeprecationWarning):
118+
"""Class for warnings about deprecated schema features in CycloneDX 1.3"""
119+
SCHEMA_VERSION: ClassVar[Literal[SchemaVersion.V1_3]] = SchemaVersion.V1_3
120+
121+
122+
class SchemaDeprecationWarning1Dot2(BaseSchemaDeprecationWarning):
123+
"""Class for warnings about deprecated schema features in CycloneDX 1.2"""
124+
SCHEMA_VERSION: ClassVar[Literal[SchemaVersion.V1_2]] = SchemaVersion.V1_2
125+
126+
127+
class SchemaDeprecationWarning1Dot1(BaseSchemaDeprecationWarning):
128+
"""Class for warnings about deprecated schema features in CycloneDX 1.1"""
129+
SCHEMA_VERSION: ClassVar[Literal[SchemaVersion.V1_1]] = SchemaVersion.V1_1

examples/complex_deserialize.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

1818
import sys
19+
import warnings
1920
from json import loads as json_loads
2021
from typing import TYPE_CHECKING
2122

@@ -24,6 +25,7 @@
2425
from cyclonedx.exception import MissingOptionalDependencyException
2526
from cyclonedx.model.bom import Bom
2627
from cyclonedx.schema import OutputFormat, SchemaVersion
28+
from cyclonedx.schema.deprecation import BaseSchemaDeprecationWarning
2729
from cyclonedx.validation import make_schemabased_validator
2830
from cyclonedx.validation.json import JsonStrictValidator
2931

@@ -154,8 +156,10 @@
154156
print('JSON valid')
155157
except MissingOptionalDependencyException as error:
156158
print('JSON-validation was skipped due to', error)
157-
bom_from_json = Bom.from_json( # type: ignore[attr-defined]
158-
json_loads(json_data))
159+
with warnings.catch_warnings():
160+
warnings.filterwarnings('ignore', category=BaseSchemaDeprecationWarning)
161+
bom_from_json = Bom.from_json( # type: ignore[attr-defined]
162+
json_loads(json_data))
159163
print('bom_from_json', repr(bom_from_json))
160164

161165
# endregion JSON
@@ -255,8 +259,10 @@
255259
print('XML valid')
256260
except MissingOptionalDependencyException as error:
257261
print('XML-validation was skipped due to', error)
258-
bom_from_xml = Bom.from_xml( # type: ignore[attr-defined]
259-
SafeElementTree.fromstring(xml_data))
262+
with warnings.catch_warnings():
263+
warnings.filterwarnings('ignore', category=BaseSchemaDeprecationWarning)
264+
bom_from_xml = Bom.from_xml( # type: ignore[attr-defined]
265+
SafeElementTree.fromstring(xml_data))
260266
print('bom_from_xml', repr(bom_from_xml))
261267

262268
# endregion XML

0 commit comments

Comments
 (0)