Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .ci/generation/py2cs/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ def patch_with(patches: list[FilePatch]) -> list[PatchResult]:
(args.out / "reports").mkdir(exist_ok=True)
for repo in plan.repos():
(args.out / "reports" / (repo.replace("/", "__") + ".md")).write_text(render_report(plan, args.python_pr, repo), encoding="utf-8")
for message in plan.existing:
print(f"note: {message}; nothing to raise (py2cs align compares it with the Python)")
for message in plan.errors:
print(f"error: {message}")
print(f"{len(plan.files)} files planned in {len(plan.repos())} repositories, {len(plan.errors)} problems")
return 1 if plan.errors else 0

Expand Down
4 changes: 3 additions & 1 deletion .ci/generation/py2cs/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class Plan:
files: list[PlannedFile] = field(default_factory=lambda: [])
removed: list[tuple[str, str]] = field(default_factory=lambda: []) # (repo or "?", clr name)
errors: list[str] = field(default_factory=lambda: [])
# New in Python, but the C# already declares them (the C# pull request was merged): nothing to raise, `align` compares them
existing: list[str] = field(default_factory=lambda: [])

def repos(self) -> list[str]:
"""Returns the sorted repositories that have a planned file or a removed type with a known repository. `plan` writes one report per repository and `pr` raises one pull request per repository.
Expand Down Expand Up @@ -97,7 +99,7 @@ def make_plan(diff: Diff, head: Manifest, base: Manifest, clone_for: Callable[[s
if found.path is not None:
relative = found.path.relative_to(root).as_posix()
if change.kind == "added":
plan.errors.append(f"{change.clr_name}: the type is new in Python, but {relative} in {repo} already declares {info.simple_name}; compare them by hand")
plan.existing.append(f"{change.clr_name}: {relative} in {repo} already declares {info.simple_name}")
continue
patch = build_patch(found.path, base[change.clr_name], info, head)
if found.note:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using PythonSchemaGeneration;
using PythonSchemaGeneration.Emission;

Expand All @@ -25,21 +28,69 @@ public static void Matches(string goldenFolder, string relativePath, string actu

public class GoldenTests
{
//One fixture class per situation the generator must handle. Classes added to the fixture repository later are not compared, so a merged
//C# change does not break these tests; add a class here only when it covers a situation that no listed class covers.
private static readonly string[] m_PinnedClasses =
{
"FixtureAlpha_oM/Beam",
"FixtureAlpha_oM/Collections",
"FixtureAlpha_oM/CycleA",
"FixtureAlpha_oM/CycleB",
"FixtureAlpha_oM/GeometryHolder",
"FixtureAlpha_oM/GridHolder",
"FixtureAlpha_oM/ICurveLike",
"FixtureAlpha_oM/IElement1DLike",
"FixtureAlpha_oM/IGeometryLike",
"FixtureAlpha_oM/ILabelled",
"FixtureAlpha_oM/IOpeningLike_1",
"FixtureAlpha_oM/IPanelLike_2",
"FixtureAlpha_oM/ImmutableThing",
"FixtureAlpha_oM/LineLike",
"FixtureAlpha_oM/Options",
"FixtureAlpha_oM/PointLike",
"FixtureAlpha_oM/ProjectLike",
"FixtureAlpha_oM/Scalars",
"FixtureAlpha_oM/Sparse",
"FixtureAlpha_oM/SparseHolder",
"FixtureAlpha_oM/SparseParent",
"FixtureAlpha_oM/SupportKind",
"FixtureAlpha_oM/ThingIdentity",
"FixtureAlpha_oM/TreeLike_1",
"FixtureAlpha_oM/Wrapper_1",
"FixtureBeta_oM/Elements/UsesBoth",
"FixtureBeta_oM/PointLike"
};

/***************************************************/

[Test]
[Description("Checks that the Python generated for every fixture type is identical to its committed golden file. Protects the exact output of the " +
"generator against unintended changes.")]
[Description("Checks that the Python generated for each pinned fixture class is identical to its committed golden file. Protects the exact output of the " +
"generator against unintended changes, without depending on the other classes of the fixture repository.")]
public void FixtureOutputMatchesGoldenFiles()
{
foreach (RenderedType type in FixtureOutput.Result.Types.Where(t => t.Ref.RelativePath.StartsWith("Fixture")))
List<RenderedType> pinned = FixtureOutput.Result.Types.Where(t => m_PinnedClasses.Contains(t.Ref.RelativePath)).ToList();
Assert.That(pinned.Select(t => t.Ref.RelativePath).OrderBy(x => x, StringComparer.Ordinal), Is.EqualTo(m_PinnedClasses), "every pinned class must still be generated");

foreach (RenderedType type in pinned)
GoldenAssert.Matches("fixtures", type.Ref.RelativePath + ".py", type.Text);
}

/***************************************************/

[Test]
[Description("Checks that the manifest written for the fixture run is identical to the committed golden manifest. Protects the language-neutral manifest " +
"that py2cs and alignment read.")]
[Description("Checks that the entries of the pinned fixture classes in the manifest written for the fixture run are identical to the committed golden manifest. " +
"Protects the language-neutral manifest that py2cs and alignment read.")]
public void FixtureManifestMatchesGolden()
{
GoldenAssert.Matches("fixtures", "manifest.json", File.ReadAllText(Path.Combine(FixtureOutput.Folder, "manifest.json")));
JsonNode manifest = JsonNode.Parse(File.ReadAllText(Path.Combine(FixtureOutput.Folder, "manifest.json")));
JsonArray types = manifest["types"].AsArray();
List<JsonNode> kept = types.Where(t => m_PinnedClasses.Contains(((string)t["relativePath"]).Replace(".py", ""))).ToList();
types.Clear();
foreach (JsonNode type in kept)
types.Add(type.DeepClone());

string text = manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }) + '\n';
GoldenAssert.Matches("fixtures", "manifest.json", text);
}
}

Expand Down
133 changes: 1 addition & 132 deletions .ci/unit-tests/golden/fixtures/manifest.json
Original file line number Diff line number Diff line change
@@ -1,137 +1,6 @@
{
"schemaVersion": 1,
"types": [
{
"clrName": "BH.oM.Base.BHoMObject",
"name": "BH.oM.Base.BHoMObject",
"pythonName": "BHoMObject",
"pythonModule": "bhom_schema.BHoM.BHoMObject",
"relativePath": "BHoM/BHoMObject.py",
"namespace": "BH.oM.Base",
"assembly": "BHoM",
"kind": "class",
"bases": [
"BH.oM.Base.IBHoMObject"
],
"repo": "BHoM/BHoM",
"properties": [
{
"name": "BHoM_Guid",
"clrType": "System.Guid",
"pyName": "BHoM_Guid",
"hasDefault": true,
"default": "<factory> uuid4"
},
{
"name": "Name",
"clrType": "string",
"pyName": "Name",
"hasDefault": true,
"default": "''"
},
{
"name": "Fragments",
"clrType": "BH.oM.Base.FragmentSet",
"pyName": "Fragments",
"hasDefault": true,
"default": "<factory> list"
},
{
"name": "Tags",
"clrType": "System.Collections.Generic.HashSet<string>",
"pyName": "Tags",
"hasDefault": true,
"default": "<factory> set"
},
{
"name": "CustomData",
"clrType": "System.Collections.Generic.Dictionary<string, object>",
"pyName": "CustomData",
"hasDefault": true,
"default": "<factory> dict"
}
]
},
{
"clrName": "BH.oM.Base.IBHoMObject",
"name": "BH.oM.Base.IBHoMObject",
"pythonName": "IBHoMObject",
"pythonModule": "bhom_schema.BHoM.IBHoMObject",
"relativePath": "BHoM/IBHoMObject.py",
"namespace": "BH.oM.Base",
"assembly": "BHoM",
"kind": "interface",
"bases": [
"BH.oM.Base.IObject"
],
"repo": "BHoM/BHoM",
"properties": [
{
"name": "BHoM_Guid",
"clrType": "System.Guid"
},
{
"name": "Name",
"clrType": "string"
},
{
"name": "Fragments",
"clrType": "BH.oM.Base.FragmentSet"
},
{
"name": "Tags",
"clrType": "System.Collections.Generic.HashSet<string>"
},
{
"name": "CustomData",
"clrType": "System.Collections.Generic.Dictionary<string, object>"
}
]
},
{
"clrName": "BH.oM.Base.IFragment",
"name": "BH.oM.Base.IFragment",
"pythonName": "IFragment",
"pythonModule": "bhom_schema.BHoM.IFragment",
"relativePath": "BHoM/IFragment.py",
"namespace": "BH.oM.Base",
"assembly": "BHoM",
"kind": "interface",
"bases": [
"BH.oM.Base.IObject"
],
"repo": "BHoM/BHoM",
"description": "Describes objects that can exist independently or can be attached to other BHoM objects as part of their `FragmentSet` property.",
"properties": []
},
{
"clrName": "BH.oM.Base.IImmutable",
"name": "BH.oM.Base.IImmutable",
"pythonName": "IImmutable",
"pythonModule": "bhom_schema.BHoM.IImmutable",
"relativePath": "BHoM/IImmutable.py",
"namespace": "BH.oM.Base",
"assembly": "BHoM",
"kind": "interface",
"bases": [
"BH.oM.Base.IObject"
],
"repo": "BHoM/BHoM",
"properties": []
},
{
"clrName": "BH.oM.Base.IObject",
"name": "BH.oM.Base.IObject",
"pythonName": "IObject",
"pythonModule": "bhom_schema.BHoM.IObject",
"relativePath": "BHoM/IObject.py",
"namespace": "BH.oM.Base",
"assembly": "BHoM",
"kind": "interface",
"bases": [],
"repo": "BHoM/BHoM",
"properties": []
},
{
"clrName": "BH.oM.FixtureAlpha.Beam",
"name": "BH.oM.FixtureAlpha.Beam",
Expand Down Expand Up @@ -1012,4 +881,4 @@
]
}
]
}
}
4 changes: 2 additions & 2 deletions .ci/unit-tests/python/test_alignment/test_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_a_package_that_differs_from_the_csharp_manifest_fails(tmp_path: Path):
"""
package = tmp_path / "bhom_schema"
shutil.copytree(require_output("fixtures"), package)
(package / "FixtureAlpha_oM" / "Beam.py").unlink()
(package / "FixtureAlpha_oM" / "Scalars.py").unlink()

_, problems = verify_package(package, OUTPUT / "fixtures" / "manifest.json")
assert "BH.oM.FixtureAlpha.Beam: missing" in problems
assert "BH.oM.FixtureAlpha.Scalars: missing" in problems
17 changes: 17 additions & 0 deletions .ci/unit-tests/python/test_py2cs/test_locate_resolve_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,23 @@ def test_plan_for_a_modified_new_and_removed_type(tmp_path: Path):
assert "## BHoM/Demo_Toolkit" in report and "property added: string ProjectNumber" in report and "BH.oM.Demo.Mode" in report


def test_a_new_python_class_that_the_csharp_already_declares_is_not_an_error(tmp_path: Path):
"""Checks that make_plan lists a class that is new in Python but already declared in the C# clone as existing, with no error and no planned file.

Protects a Python pull request whose linked C# pull request is merged from failing when it is pushed to again.
"""
clones = tmp_path / "clones"
repo = csharp_tree(clones)
base = read_package(demo_package(tmp_path / "base"))
head_root = demo_package(tmp_path / "head")
(head_root / "Demo_oM" / "Door.py").write_text(module("Door", "BH.oM.Demo.Elements.Door", "BH.oM.Demo.Elements", "Demo_oM", body=["X: float = 0.0"]), encoding="utf-8")
head = read_package(head_root)

plan = make_plan(diff_manifests(base, head), head, base, lambda r: repo, {"BHoM/Demo_Toolkit": ["Demo_oM"]})
assert plan.errors == [] and plan.files == []
assert len(plan.existing) == 1 and plan.existing[0].startswith("BH.oM.Demo.Elements.Door: ")


def test_plan_reports_problems_instead_of_guessing(tmp_path: Path):
"""Checks that make_plan reports an error when there is no checkout of the repository or when it is not in the listing, and plans no files.

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/linked-csharp-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ jobs:
id: token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.BHOM_APP_ID }}
app-id: ${{ secrets.BHOM_APP_ID }}
private-key: ${{ secrets.BHOM_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ matrix.name }}
Expand Down Expand Up @@ -218,7 +218,7 @@ jobs:
id: token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.BHOM_APP_ID }}
app-id: ${{ secrets.BHOM_APP_ID }}
private-key: ${{ secrets.BHOM_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ matrix.name }}
Expand Down
2 changes: 1 addition & 1 deletion docs/CODING-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ this repository.
3. **Store the credentials.** Note the App ID (or Client ID) and generate a private key (`.pem`). Add them where the workflow can
read them: either an organisation secret/variable shared with `BHoM_PythonSchema`, or repository-level ones. Suggested
names (reuse the names your other workflows already use if the secret exists):
- variable `BHOM_APP_ID`
- secret `BHOM_APP_ID`
- secret `BHOM_APP_PRIVATE_KEY` (the full PEM text including the BEGIN/END lines)
4. **Protect the secrets.** Optionally attach them to a GitHub **Environment** (for example `linked-csharp-pr`) so only the
linked-PR job can read them, with a required reviewer if you want a human gate before anything is raised.
Expand Down
2 changes: 1 addition & 1 deletion docs/Testing-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ yet, or fails it when the `CI` environment variable is `true`. That is the Pytho
A golden file is a committed copy of expected output. The test generates the output, compares it with the file, and fails on any
difference. Use `golden/` when exact text matters:

- `golden/fixtures/`: the Python generated for the fixture types, and the C# manifest.
- `golden/fixtures/`: the Python generated for a pinned list of fixture classes (one per situation, listed in `GoldenTests.m_PinnedClasses`), and their entries of the C# manifest. Classes added to CSharpSchema_Test later are not compared, so a merged C# change never needs a golden update; add a class to the list only for a situation no listed class covers.
- `golden/spike/`: a small, stable real type (`GradientCenteringOptions`).
- `golden/py2cs/`: the C# that `py2cs` writes for small example classes.

Expand Down
2 changes: 2 additions & 0 deletions docs/Testing-On-GitHub.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ prints what it would do and proves the App token; unticked it does it.
3. Re-run **pr-checks**. `py2cs align` must now pass: the Python generated from the merged C# equals the Python in the PR.
If it still fails, the message lists what differs; either the C# review changed something (copy the change into the Python)
or the generator and `py2cs` disagree (a bug to fix in this repository).
The NUnit golden files cover a pinned list of fixture classes only, so a class merged into CSharpSchema_Test does not break them and no golden
update is needed. A test that deletes a fixture class file must pick a class that no other fixture imports.
4. Merge the Python PR.
5. Run *Python schema generation* live (or wait for Saturday). Expected: `No changes: the Python already matches the C#`.
If it commits, the commit shows exactly how the generated Python differs from what was written by hand (often only formatting or
Expand Down
4 changes: 2 additions & 2 deletions docs/Workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ Someone with admin rights on the organisation does this once.
1. **A GitHub App** that can push a branch and open pull requests. Permissions: *Contents: read and write*, *Pull requests: read and
write*, *Metadata: read*. Install it on **BHoM_PythonSchema and CSharpSchema_Test only**. A repository outside the installation
cannot get a token, so the App cannot write to it.
2. **Credentials** in this repository (Settings, Secrets and variables, Actions): variable `BHOM_APP_ID` and secret
`BHOM_APP_PRIVATE_KEY` (the whole `.pem` text). If the App already exists for other workflows, reuse its names.
2. **Credentials** in this repository (Settings, Secrets and variables, Actions): secrets `BHOM_APP_ID` and
`BHOM_APP_PRIVATE_KEY` (the whole `.pem` text). The organisation may already hold both under these names for other workflows; a repository secret overrides it.
3. **`develop` must accept the weekly commit.** The generation pushes to `develop` with the workflow's own token. If `develop` is
protected and requires pull requests, allow `github-actions[bot]` (or the App) to bypass that rule.
4. **Actions** are enabled, and the default workflow permissions can stay read-only: each workflow asks for what it needs.
Expand Down
27 changes: 27 additions & 0 deletions src/bhom_schema/FixtureAlpha_oM/BeamPair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by BHoM_PythonSchema from the C# oM type BH.oM.FixtureAlpha.Beam. Do not edit by hand.
# Copyright (c) 2015 - 2026, the respective contributors. All rights reserved.
# Licensed under the GNU Lesser General Public License, version 3.0 or later.
# pyright: reportUnknownVariableType=false, reportIncompatibleVariableOverride=false

from __future__ import annotations

from typing import Any, ClassVar

from pydantic import Field

from bhom_schema.BHoM.BHoMObject import BHoMObject
from bhom_schema.FixtureAlpha_oM.Beam import Beam


class BeamPair(BHoMObject):
__bhom__: ClassVar[dict[str, Any]] = {
'clr_name': 'BH.oM.FixtureAlpha.BeamPair',
'assembly': 'FixtureAlpha_oM',
'namespace': 'BH.oM.FixtureAlpha',
'repo': 'BHoM/CSharpSchema_Test',
'kind': 'class',
}

First: Beam | None = Field(default=None, description='The first beam of the pair.')
Second: Beam | None = Field(default=None, description='The second beam of the pair.')

Loading