Skip to content

Commit 9a6529a

Browse files
authored
👷‍♂️Include only functional releases into compatibility matrix (#802)
* 👷‍♂️Include only functional releases into compatibility matrix * 🚨mypy * Make it work again * Fetch all for doc test * Default case current checkout commit * 🚨pylint
1 parent 6f23384 commit 9a6529a

File tree

6 files changed

+278
-196
lines changed

6 files changed

+278
-196
lines changed

.github/workflows/docs_latest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- name: Checkout
3535
uses: actions/checkout@v4
3636
with:
37-
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
37+
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
3838
- name: Install dependencies
3939
run: |
4040
python -m pip install --upgrade pip

.github/workflows/test_docs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ jobs:
1010
os: [ubuntu-latest]
1111
steps:
1212
- uses: actions/checkout@v4
13+
with:
14+
fetch-depth: 0
1315
- name: Set up Python ${{ matrix.python-version }}
1416
uses: actions/setup-python@v5
1517
with:

docs/compatibility/__main__.py

Lines changed: 11 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -6,171 +6,21 @@
66
if you're testing locally.
77
"""
88

9-
import itertools
109
import logging
1110
import re
12-
import shutil
1311
from pathlib import Path
14-
from typing import Any as _Any
15-
from typing import Iterable
1612

1713
import bost.operations
18-
from bost import main as bost_main
19-
from bost.operations import update_references as bost_update_references
20-
from bost.pull import OWNER, REPO, SchemaMetadata, get_source_repo
14+
from bost.pull import OWNER, REPO
2115

22-
from . import change_schemas, diff, loader, matrix
16+
from . import diff, loader, matrix, versioning
2317

24-
BO4E_BASE_DIR = Path(__file__).parents[2] / "tmp/bo4e_json_schemas"
25-
LOCAL_JSON_SCHEMA_DIR = Path(__file__).parents[2] / "json_schemas"
2618
logger = logging.getLogger(__name__)
2719

28-
29-
def pull_bo4e_version(version: str, output: Path, gh_token: str | None = None) -> None:
30-
"""
31-
Pull the BO4E version from the given version string.
32-
"""
33-
bost_main(
34-
output=output,
35-
target_version=version,
36-
update_refs=True,
37-
set_default_version=False,
38-
clear_output=True,
39-
token=gh_token,
40-
)
41-
42-
43-
def update_references(path: Path, version: str) -> None:
44-
"""
45-
Update the references in the given path. This step is needed for the local build.
46-
"""
47-
schema_namespace = {}
48-
for schema_path in loader.get_namespace(path):
49-
local_path = Path(path, *schema_path).with_suffix(".json")
50-
schema_namespace[schema_path[-1]] = SchemaMetadata(
51-
class_name=schema_path[-1],
52-
download_url="",
53-
module_path=schema_path,
54-
file_path=local_path,
55-
cached_path=local_path,
56-
token=None,
57-
)
58-
for schema_metadata in schema_namespace.values():
59-
bost_update_references(schema_metadata, schema_namespace, version)
60-
schema_metadata.save()
61-
62-
63-
def pull_or_reuse_bo4e_version(version: str, gh_token: str | None = None, from_local: bool = False) -> Path:
64-
"""
65-
Pull the BO4E version from the given version string or reuse the version if it was already pulled before.
66-
If version is None use the BO4E version of the checkout working directory by assuming the compiled json
67-
schemas in /json_schemas.
68-
Returns the path of the bo4e directory.
69-
"""
70-
bo4e_dir = BO4E_BASE_DIR / version
71-
72-
if from_local:
73-
if not any(LOCAL_JSON_SCHEMA_DIR.rglob("*.json")):
74-
raise ValueError(
75-
"No local json schemas found in /json_schemas. "
76-
"Please ensure that the json schemas are build on beforehand."
77-
)
78-
if bo4e_dir.exists():
79-
shutil.rmtree(bo4e_dir)
80-
shutil.copytree(LOCAL_JSON_SCHEMA_DIR, bo4e_dir)
81-
update_references(bo4e_dir, version)
82-
elif any(bo4e_dir.rglob("*.json")):
83-
return bo4e_dir
84-
else:
85-
pull_bo4e_version(version, bo4e_dir, gh_token)
86-
return bo4e_dir
87-
88-
89-
def compare_bo4e_versions(
90-
version_old: str, version_new: str, gh_token: str | None = None, from_local: bool = False
91-
) -> Iterable[change_schemas.Change]:
92-
"""
93-
Compare the old version with the new version.
94-
If version_new is None use the BO4E version of the checkout working directory by assuming the compiled json
95-
schemas in /json_schemas.
96-
"""
97-
dir_old_schemas = pull_or_reuse_bo4e_version(version_old, gh_token)
98-
dir_new_schemas = pull_or_reuse_bo4e_version(version_new, gh_token, from_local=from_local)
99-
print(f"Comparing {version_old} with {version_new}")
100-
yield from diff.diff_schemas(dir_old_schemas, dir_new_schemas)
101-
102-
103-
def compare_bo4e_versions_iteratively(
104-
versions: Iterable[str], cur_version: str | None = None, gh_token: str | None = None
105-
) -> dict[tuple[str, str], Iterable[change_schemas.Change]]:
106-
"""
107-
Compare the versions iteratively. Each version at index i will be compared to the version at index i+1.
108-
Additionally, if cur_version is provided, the last version in the list will be compared to the version
109-
in the checkout working directory. The value of cur_version will be used to set the key in the returned
110-
dict.
111-
Note:
112-
- versions must contain at least one element.
113-
- versions should be sorted in ascending order.
114-
- if using cur_version, ensure that the json schemas of the checkout working directory
115-
were build on beforehand. They should be located in /json_schemas.
116-
"""
117-
print(f"Comparing versions {versions} with cur_version {cur_version}")
118-
changes = {}
119-
last_version: str = "" # This value is never used but makes mypy and pylint happy
120-
for version_old, version_new in itertools.pairwise(versions):
121-
last_version = version_new
122-
changes[version_old, version_new] = compare_bo4e_versions(version_old, version_new, gh_token)
123-
if cur_version is not None:
124-
changes[last_version, cur_version] = compare_bo4e_versions(last_version, cur_version, gh_token, from_local=True)
125-
print("Comparisons finished.")
126-
return changes
127-
128-
12920
REGEX_RELEASE_VERSION = re.compile(r"^v(\d{6}\.\d+\.\d+)$")
13021
REGEX_RELEASE_CANDIDATE_VERSION = re.compile(r"^v(\d{6}\.\d+\.\d+)-rc\d+$")
13122

13223

133-
def get_last_n_release_versions(n: int, include_rc: bool = False, gh_token: str | None = None) -> Iterable[str]:
134-
"""
135-
Get the last n release versions from the BO4E repository.
136-
"""
137-
repo = get_source_repo(gh_token)
138-
releases = repo.get_releases()
139-
counter = 0
140-
141-
for release in releases:
142-
if not REGEX_RELEASE_VERSION.fullmatch(release.tag_name) and (
143-
not include_rc or not REGEX_RELEASE_CANDIDATE_VERSION.fullmatch(release.tag_name)
144-
):
145-
continue
146-
counter += 1
147-
yield release.tag_name
148-
if counter >= n:
149-
return
150-
151-
logger.warning("Only %d matching releases found. Returning all releases.", counter)
152-
153-
154-
def get_all_release_versions_since_20240100(include_rc: bool = False, gh_token: str | None = None) -> Iterable[str]:
155-
"""
156-
Get all release versions since v202401.0.0 from the BO4E repository.
157-
"""
158-
repo = get_source_repo(gh_token)
159-
releases = repo.get_releases()
160-
version_threshold = "v202401.0.0"
161-
162-
for release in releases:
163-
if not REGEX_RELEASE_VERSION.fullmatch(release.tag_name) and (
164-
not include_rc or not REGEX_RELEASE_CANDIDATE_VERSION.fullmatch(release.tag_name)
165-
):
166-
continue
167-
yield release.tag_name
168-
if release.tag_name == version_threshold:
169-
return
170-
171-
logger.warning("Threshold version %s not found. Returned all matching releases.", version_threshold)
172-
173-
17424
def _monkey_patch_bost_regex_if_local_testing(version: str) -> None:
17525
regex_expected_version = re.compile(r"^v\d+\.\d+\.\d+(?:-rc\d+)?$")
17626
if not regex_expected_version.fullmatch(version):
@@ -196,20 +46,20 @@ def create_tables_for_doc(
19646
The compatibility matrix will be built for last_n_versions + the current version in the checkout working directory.
19747
If you set last_n_versions = 0 all versions since v202401.0.0 will be compared.
19848
Note: The matrix will never contain the first version as column. Each column is a comparison to the version before.
199-
Note: Release candidates are excluded.
49+
Note: Only functional releases will be compared since technical releases are enforced to be fully compatible.
50+
See https://github.com/bo4e/BO4E-python/issues/784
20051
"""
20152
_monkey_patch_bost_regex_if_local_testing(gh_version)
20253
logger.info("Retrieving the last %d release versions", last_n_versions)
203-
if last_n_versions > 0:
204-
versions = list(reversed(list(get_last_n_release_versions(last_n_versions, gh_token=gh_token))))
205-
else:
206-
versions = list(reversed(list(get_all_release_versions_since_20240100(gh_token=gh_token))))
54+
versions = list(
55+
reversed(list(versioning.get_last_n_tags(last_n_versions, ref=gh_version, exclude_technical_bumps=True)))
56+
)
20757
logger.info("Comparing versions iteratively: %s", " -> ".join([*versions, gh_version]))
208-
changes_iterables = compare_bo4e_versions_iteratively(versions, gh_version, gh_token=gh_token)
58+
changes_iterables = diff.compare_bo4e_versions_iteratively(versions, gh_version, gh_token=gh_token)
20959
logger.info("Building namespaces")
21060
changes = {key: list(value) for key, value in changes_iterables.items()}
211-
namespaces = {version: list(loader.get_namespace(BO4E_BASE_DIR / version)) for version in versions}
212-
namespaces[gh_version] = list(loader.get_namespace(BO4E_BASE_DIR / gh_version))
61+
namespaces = {version: list(loader.get_namespace(loader.BO4E_BASE_DIR / version)) for version in versions}
62+
namespaces[gh_version] = list(loader.get_namespace(loader.BO4E_BASE_DIR / gh_version))
21363
logger.info("Creating compatibility matrix")
21464
matrix.create_compatibility_matrix_csv(
21565
compatibility_matrix_output_file, [*versions, gh_version], namespaces, changes
@@ -224,5 +74,5 @@ def test_create_tables_for_doc() -> None:
22474
create_tables_for_doc(
22575
Path(__file__).parents[1] / "compatibility_matrix.csv",
22676
"local",
227-
last_n_versions=3,
77+
last_n_versions=0,
22878
)

docs/compatibility/diff.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
Contains the logic to detect the different changes between two BO4E versions.
33
"""
44

5+
import itertools
56
import re
67
from pathlib import Path
7-
from typing import Any as _Any
8-
from typing import Iterable
8+
from typing import Iterable, Sequence
99

1010
from bost.schema import AllOf, AnyOf, Array, Object, Reference, SchemaRootType, SchemaType, StrEnum, String, TypeBase
1111

@@ -320,3 +320,46 @@ def diff_schemas(schemas_old: Path, schemas_new: Path) -> Iterable[change_schema
320320
f"{'/'.join(schema_file.with_suffix('').parts)}#",
321321
f"{'/'.join(schema_file.with_suffix('').parts)}#",
322322
)
323+
324+
325+
def compare_bo4e_versions(
326+
version_old: str, version_new: str, gh_token: str | None = None, from_local: bool = False
327+
) -> Iterable[change_schemas.Change]:
328+
"""
329+
Compare the old version with the new version.
330+
If version_new is None use the BO4E version of the checkout working directory by assuming the compiled json
331+
schemas in /json_schemas.
332+
"""
333+
dir_old_schemas = loader.pull_or_reuse_bo4e_version(version_old, gh_token)
334+
dir_new_schemas = loader.pull_or_reuse_bo4e_version(version_new, gh_token, from_local=from_local)
335+
print(f"Comparing {version_old} with {version_new}")
336+
yield from diff_schemas(dir_old_schemas, dir_new_schemas)
337+
338+
339+
def compare_bo4e_versions_iteratively(
340+
versions: Sequence[str], cur_version: str | None = None, gh_token: str | None = None
341+
) -> dict[tuple[str, str], Iterable[change_schemas.Change]]:
342+
"""
343+
Compare the versions iteratively. Each version at index i will be compared to the version at index i+1.
344+
Additionally, if cur_version is provided, the last version in the list will be compared to the version
345+
in the checkout working directory. The value of cur_version will be used to set the key in the returned
346+
dict.
347+
Note:
348+
- versions must contain at least one element.
349+
- versions should be sorted in ascending order.
350+
- if using cur_version, ensure that the json schemas of the checkout working directory
351+
were build on beforehand. They should be located in /json_schemas.
352+
"""
353+
print(f"Comparing versions {versions} with cur_version {cur_version}")
354+
if len(versions) == 0:
355+
print("No versions to compare.")
356+
return {}
357+
changes = {}
358+
last_version: str = versions[0] # This value is never used but makes mypy and pylint happy
359+
for version_old, version_new in itertools.pairwise(versions):
360+
last_version = version_new
361+
changes[version_old, version_new] = compare_bo4e_versions(version_old, version_new, gh_token)
362+
if cur_version is not None:
363+
changes[last_version, cur_version] = compare_bo4e_versions(last_version, cur_version, gh_token, from_local=True)
364+
print("Comparisons finished.")
365+
return changes

docs/compatibility/loader.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@
33
"""
44

55
import json
6+
import shutil
67
from pathlib import Path
78
from typing import Iterable
89

10+
from bost import main as bost_main
11+
from bost.operations import update_references as bost_update_references
12+
from bost.pull import SchemaMetadata
913
from bost.schema import SchemaRootType
1014
from pydantic import TypeAdapter
1115

1216
from . import change_schemas
1317

18+
BO4E_BASE_DIR = Path(__file__).parents[2] / "tmp/bo4e_json_schemas"
19+
LOCAL_JSON_SCHEMA_DIR = Path(__file__).parents[2] / "json_schemas"
20+
1421

1522
def load_schema_file(path: Path) -> SchemaRootType:
1623
"""
@@ -45,3 +52,63 @@ def get_namespace(path: Path) -> Iterable[tuple[str, ...]]:
4552
for schema_file in path.rglob("*.json"):
4653
sub_path = schema_file.relative_to(path).parts[:-1]
4754
yield *sub_path, schema_file.stem
55+
56+
57+
def pull_bo4e_version(version: str, output: Path, gh_token: str | None = None) -> None:
58+
"""
59+
Pull the BO4E version from the given version string.
60+
"""
61+
bost_main(
62+
output=output,
63+
target_version=version,
64+
update_refs=True,
65+
set_default_version=False,
66+
clear_output=True,
67+
token=gh_token,
68+
)
69+
70+
71+
def update_references(path: Path, version: str) -> None:
72+
"""
73+
Update the references in the given path. This step is needed for the local build.
74+
"""
75+
schema_namespace = {}
76+
for schema_path in get_namespace(path):
77+
local_path = Path(path, *schema_path).with_suffix(".json")
78+
schema_namespace[schema_path[-1]] = SchemaMetadata(
79+
class_name=schema_path[-1],
80+
download_url="",
81+
module_path=schema_path,
82+
file_path=local_path,
83+
cached_path=local_path,
84+
token=None,
85+
)
86+
for schema_metadata in schema_namespace.values():
87+
bost_update_references(schema_metadata, schema_namespace, version)
88+
schema_metadata.save()
89+
90+
91+
def pull_or_reuse_bo4e_version(version: str, gh_token: str | None = None, from_local: bool = False) -> Path:
92+
"""
93+
Pull the BO4E version from the given version string or reuse the version if it was already pulled before.
94+
If version is None use the BO4E version of the checkout working directory by assuming the compiled json
95+
schemas in /json_schemas.
96+
Returns the path of the bo4e directory.
97+
"""
98+
bo4e_dir = BO4E_BASE_DIR / version
99+
100+
if from_local:
101+
if not any(LOCAL_JSON_SCHEMA_DIR.rglob("*.json")):
102+
raise ValueError(
103+
"No local json schemas found in /json_schemas. "
104+
"Please ensure that the json schemas are build on beforehand."
105+
)
106+
if bo4e_dir.exists():
107+
shutil.rmtree(bo4e_dir)
108+
shutil.copytree(LOCAL_JSON_SCHEMA_DIR, bo4e_dir)
109+
update_references(bo4e_dir, version)
110+
elif any(bo4e_dir.rglob("*.json")):
111+
return bo4e_dir
112+
else:
113+
pull_bo4e_version(version, bo4e_dir, gh_token)
114+
return bo4e_dir

0 commit comments

Comments
 (0)