From cf93c7026c1c8ad2f4337e90eb804338c8c7a2a4 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 4 Jul 2026 16:40:55 +0100 Subject: [PATCH 1/9] Go: Add go vet and staticheck to test Signed-off-by: Arthit Suriyawongkul --- .github/workflows/test.yaml | 4 ++ pyproject.toml | 1 + tests/test_golang.py | 82 +++++++++++++++++++++++++++++-------- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c52f6a89..703de3d4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -50,6 +50,10 @@ jobs: run: | brew update brew install doxygen gcc go graphviz + - name: Install Go static analysis tools + run: | + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + go install honnef.co/go/tools/cmd/staticcheck@latest - name: Build package run: | python -m build diff --git a/pyproject.toml b/pyproject.toml index 6695adc9..4b685d82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ addopts = [ "-n", "auto", "--dist=loadfile", + "-ra", ] # Suppress 3rd-party warnings to reduce log noise. Remove after libraries get updated. filterwarnings = [ diff --git a/tests/test_golang.py b/tests/test_golang.py index efbd7276..eb0c61c3 100644 --- a/tests/test_golang.py +++ b/tests/test_golang.py @@ -474,13 +474,35 @@ def link_test(test_lib, tmp_path_factory): ) -@pytest.mark.parametrize( +GOLANG_MODEL_TESTS = ( "args", [ ["--input", TEST_MODEL], ["--input", TEST_MODEL, "--context-url", TEST_CONTEXT, SPDX3_CONTEXT_URL], ], ) + + +def _build_golang_module(tmp_path, args): + subprocess.run( + [ + "shacl2code", + "generate", + ] + + args + + [ + "golang", + "--output", + tmp_path, + ], + check=True, + ) + + subprocess.run(["go", "mod", "init", "model"], cwd=tmp_path, check=True) + subprocess.run(["go", "mod", "tidy"], cwd=tmp_path, check=True) + + +@pytest.mark.parametrize(*GOLANG_MODEL_TESTS) class TestOutput: def test_trailing_whitespace(self, tmp_path, args): """ @@ -509,22 +531,7 @@ def test_trailing_whitespace(self, tmp_path, args): ), f"{fn}: Line {num + 1} has trailing whitespace: {line!r}" def test_output_compile(self, tmp_path, args): - subprocess.run( - [ - "shacl2code", - "generate", - ] - + args - + [ - "golang", - "--output", - tmp_path, - ], - check=True, - ) - - subprocess.run(["go", "mod", "init", "model"], cwd=tmp_path, check=True) - subprocess.run(["go", "mod", "tidy"], cwd=tmp_path, check=True) + _build_golang_module(tmp_path, args) model_output = tmp_path / "model.a" @@ -535,6 +542,47 @@ def test_output_compile(self, tmp_path, args): ) +@pytest.mark.parametrize(*GOLANG_MODEL_TESTS) +class TestCheckType: + """ + Static analysis checks for the generated Go code + """ + + def test_vet(self, tmp_path, args): + """ + go vet static analysis + """ + _build_golang_module(tmp_path, args) + subprocess.run(["go", "vet", "./..."], cwd=tmp_path, check=True) + + def test_staticcheck(self, tmp_path, args): + """ + staticcheck static analysis. + + Disabled: + - ST1003 (snake_case names): generated identifiers mirror the SHACL + model's own naming. + - ST1006 ("self" receiver name): generated methods use "self" to + match other language bindings. + + Any other finding is reported as an xfail so it's visible + in the test report. + """ + _build_golang_module(tmp_path, args) + p = subprocess.run( + ["staticcheck", "-checks=all,-ST1003,-ST1006", "./..."], + cwd=tmp_path, + capture_output=True, + encoding="utf-8", + ) + if p.returncode != 0: + pytest.xfail( + "staticcheck reported findings against the generated code " + "(see golang templates in src/shacl2code/lang/templates/" + "golang/):\n" + p.stdout + p.stderr + ) + + def test_compile(compile_test): compile_test("") From 4090ace28e13149b7bf7ac89cafc59bfd2730ea5 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 4 Jul 2026 17:56:41 +0100 Subject: [PATCH 2/9] Install staticcheck in coverage gen workflow Signed-off-by: Arthit Suriyawongkul --- .github/workflows/coverage-generate.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/coverage-generate.yaml b/.github/workflows/coverage-generate.yaml index 68570c1e..9a14e6d8 100644 --- a/.github/workflows/coverage-generate.yaml +++ b/.github/workflows/coverage-generate.yaml @@ -37,6 +37,10 @@ jobs: sudo apt install -y build-essential doxygen graphviz npm install -g jsonld-cli npm --prefix scripts ci + - name: Install Go static analysis tools + run: | + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + go install honnef.co/go/tools/cmd/staticcheck@latest - name: Install package run: | pip install -e .[dev] From 8f525eec935cce59274bffb3832926f16d4dac32 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 4 Jul 2026 18:02:45 +0100 Subject: [PATCH 3/9] Style violation will fail the test Signed-off-by: Arthit Suriyawongkul --- tests/test_golang.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/test_golang.py b/tests/test_golang.py index eb0c61c3..3bb43943 100644 --- a/tests/test_golang.py +++ b/tests/test_golang.py @@ -565,22 +565,15 @@ def test_staticcheck(self, tmp_path, args): - ST1006 ("self" receiver name): generated methods use "self" to match other language bindings. - Any other finding is reported as an xfail so it's visible - in the test report. + Any other finding fails the test so it shows up as a failed check on + the PR. """ _build_golang_module(tmp_path, args) - p = subprocess.run( + subprocess.run( ["staticcheck", "-checks=all,-ST1003,-ST1006", "./..."], cwd=tmp_path, - capture_output=True, - encoding="utf-8", + check=True, ) - if p.returncode != 0: - pytest.xfail( - "staticcheck reported findings against the generated code " - "(see golang templates in src/shacl2code/lang/templates/" - "golang/):\n" + p.stdout + p.stderr - ) def test_compile(compile_test): From ac829b2689e40ab8dbbb7d67475edced8b326c01 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 4 Jul 2026 20:31:39 +0100 Subject: [PATCH 4/9] Rename TestCheckType -> TestStaticAnalysis Signed-off-by: Arthit Suriyawongkul --- tests/test_golang.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_golang.py b/tests/test_golang.py index 3bb43943..346a3715 100644 --- a/tests/test_golang.py +++ b/tests/test_golang.py @@ -543,7 +543,7 @@ def test_output_compile(self, tmp_path, args): @pytest.mark.parametrize(*GOLANG_MODEL_TESTS) -class TestCheckType: +class TestStaticAnalysis: """ Static analysis checks for the generated Go code """ From b382baeff46b11ddd79f80b3cdea6354efef3f6c Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 4 Jul 2026 21:07:31 +0100 Subject: [PATCH 5/9] Remove stale -ra from pytest flag Signed-off-by: Arthit Suriyawongkul --- pyproject.toml | 1 - tests/test_golang.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4b685d82..6695adc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,6 @@ addopts = [ "-n", "auto", "--dist=loadfile", - "-ra", ] # Suppress 3rd-party warnings to reduce log noise. Remove after libraries get updated. filterwarnings = [ diff --git a/tests/test_golang.py b/tests/test_golang.py index 346a3715..751dba2c 100644 --- a/tests/test_golang.py +++ b/tests/test_golang.py @@ -1,6 +1,10 @@ # # Copyright (c) 2024 Joshua Watt # +# SPDX-FileContributor: Joshua Watt +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2024 Joshua Watt +# SPDX-FileType: SOURCE # SPDX-License-Identifier: MIT import json From 6ba48bc736dec5d5c9bb4f33e389561658b020b4 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 7 Jul 2026 19:06:46 +0100 Subject: [PATCH 6/9] Fix static analysis errors Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/lang/golang.py | 2 +- .../lang/templates/golang/classes.go.j2 | 32 +++++++++++++------ .../lang/templates/golang/decode.go.j2 | 15 ++++----- .../lang/templates/golang/encode.go.j2 | 2 +- .../lang/templates/golang/errorhandler.go.j2 | 2 +- .../lang/templates/golang/errors.go.j2 | 2 +- .../lang/templates/golang/extensible.go.j2 | 6 ++-- .../lang/templates/golang/linkstate.go.j2 | 2 +- .../lang/templates/golang/listproperty.go.j2 | 4 +-- .../lang/templates/golang/optional.go.j2 | 2 +- .../lang/templates/golang/path.go.j2 | 2 +- .../lang/templates/golang/property.go.j2 | 2 +- .../lang/templates/golang/ref.go.j2 | 2 +- .../templates/golang/reflistproperty.go.j2 | 2 +- .../lang/templates/golang/refproperty.go.j2 | 2 +- .../lang/templates/golang/shaclobject.go.j2 | 4 +-- .../templates/golang/shaclobjectset.go.j2 | 9 +++--- .../lang/templates/golang/shacltype.go.j2 | 2 +- .../lang/templates/golang/util.go.j2 | 2 +- .../lang/templates/golang/validator.go.j2 | 15 ++++----- 20 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/shacl2code/lang/golang.py b/src/shacl2code/lang/golang.py index ad3e3700..a10d11ec 100644 --- a/src/shacl2code/lang/golang.py +++ b/src/shacl2code/lang/golang.py @@ -242,9 +242,9 @@ def __init__(self, args): # template "go_package": textwrap.dedent(f""" */ - package {args.package} /*"""), + "package_name": args.package, } @classmethod diff --git a/src/shacl2code/lang/templates/golang/classes.go.j2 b/src/shacl2code/lang/templates/golang/classes.go.j2 index 8005cadc..4d245d13 100644 --- a/src/shacl2code/lang/templates/golang/classes.go.j2 +++ b/src/shacl2code/lang/templates/golang/classes.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ @@ -14,10 +14,17 @@ import ( /*{{ '*' }}{{ '/' }} {% for class in classes %} -{%- for l in class.comment.splitlines() %} -{{ ("// " + l).rstrip() }} -{% endfor -%} +{%- set class_comment_lines = class.comment.splitlines() %} +{%- if class_comment_lines %} +// {{ (struct_name(class) + " " + class_comment_lines[0]).rstrip() }} +{%- for l in class_comment_lines[1:] %} +// {{ l }} +{%- endfor %} +{%- else %} +// {{ struct_name(class) }} is a generated SHACL object type +{%- endif %} {%- if class.deprecated %} +// // Deprecated: This class is deprecated {%- endif %} {% set is_public = not class.is_abstract or class.is_extensible -%} @@ -44,10 +51,17 @@ type {{ struct_name(class) }} struct { {%- endfor %} } {% for member in class.named_individuals %} -{%- for l in member.comment.splitlines() %} -{{ ("// " + l).rstrip() }} -{% endfor -%} -const {{ varname(*class.clsname) }}{{ varname(member.varname) }} = "{{ member._id }}" +{%- set constname = varname(*class.clsname) + varname(member.varname) %} +{%- set comment_lines = member.comment.splitlines() %} +{%- if comment_lines %} +// {{ (constname + " " + comment_lines[0]).rstrip() }} +{%- for l in comment_lines[1:] %} +// {{ l }} +{%- endfor %} +{% else %} +// {{ constname }} is a generated named individual value +{% endif -%} +const {{ constname }} = "{{ member._id }}" {%- endfor %} type {{ struct_name(class) }}Type struct { @@ -184,7 +198,7 @@ func Make{{ varname(*class.clsname) }}Ref() Ref[{{ interface_name(class) }}] { {%- endif %} func (self *{{ struct_name(class) }}) Validate(path Path, handler ErrorHandler) bool { - var valid bool = true + var valid = true {%- if class.parent_ids %} {%- for p in class.parent_ids %} if ! self.{{ struct_name(classes.get(p)) }}.Validate(path, handler) { diff --git a/src/shacl2code/lang/templates/golang/decode.go.j2 b/src/shacl2code/lang/templates/golang/decode.go.j2 index 03aa6abb..3925af9d 100644 --- a/src/shacl2code/lang/templates/golang/decode.go.j2 +++ b/src/shacl2code/lang/templates/golang/decode.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ @@ -230,11 +230,10 @@ func DecodeInteger(data any, path Path, context map[string]string, check DecodeC return 0, err } - switch data.(type) { + switch v := data.(type) { case int: - return data.(int), nil + return v, nil case float64: - v := data.(float64) if v == float64(int64(v)) { return int(v), nil } @@ -249,15 +248,15 @@ func DecodeFloat(data any, path Path, context map[string]string, check DecodeChe return 0, err } - switch data.(type) { + switch v := data.(type) { case float64: - return data.(float64), nil + return v, nil case string: - v, err := strconv.ParseFloat(data.(string), 64) + f, err := strconv.ParseFloat(v, 64) if err != nil { return 0, err } - return v, nil + return f, nil default: return 0, &DecodeError{path, "Float expected. Got " + reflect.TypeOf(data).Name()} } diff --git a/src/shacl2code/lang/templates/golang/encode.go.j2 b/src/shacl2code/lang/templates/golang/encode.go.j2 index 09d18fab..e76da450 100644 --- a/src/shacl2code/lang/templates/golang/encode.go.j2 +++ b/src/shacl2code/lang/templates/golang/encode.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/errorhandler.go.j2 b/src/shacl2code/lang/templates/golang/errorhandler.go.j2 index 0e796f66..856db1c3 100644 --- a/src/shacl2code/lang/templates/golang/errorhandler.go.j2 +++ b/src/shacl2code/lang/templates/golang/errorhandler.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/errors.go.j2 b/src/shacl2code/lang/templates/golang/errors.go.j2 index d1c4ec7c..76a99dca 100644 --- a/src/shacl2code/lang/templates/golang/errors.go.j2 +++ b/src/shacl2code/lang/templates/golang/errors.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/extensible.go.j2 b/src/shacl2code/lang/templates/golang/extensible.go.j2 index dddf7ee0..0a8d12cf 100644 --- a/src/shacl2code/lang/templates/golang/extensible.go.j2 +++ b/src/shacl2code/lang/templates/golang/extensible.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ @@ -34,9 +34,7 @@ func (self *SHACLExtensibleBase) EncodeExtProperties(data map[string]any, path P } lst := []any{} - for _, v := range values { - lst = append(lst, v) - } + lst = append(lst, values...) data[state.CompactIRI(k)] = lst } return nil diff --git a/src/shacl2code/lang/templates/golang/linkstate.go.j2 b/src/shacl2code/lang/templates/golang/linkstate.go.j2 index a3273c54..902954f1 100644 --- a/src/shacl2code/lang/templates/golang/linkstate.go.j2 +++ b/src/shacl2code/lang/templates/golang/linkstate.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/listproperty.go.j2 b/src/shacl2code/lang/templates/golang/listproperty.go.j2 index b481a7ea..d8bc94af 100644 --- a/src/shacl2code/lang/templates/golang/listproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/listproperty.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ @@ -70,7 +70,7 @@ func (self *ListProperty[T]) Delete() { } func (self *ListProperty[T]) IsSet() bool { - return self.value != nil && len(self.value) > 0 + return len(self.value) > 0 } func (self *ListProperty[T]) Check(path Path, handler ErrorHandler) bool { diff --git a/src/shacl2code/lang/templates/golang/optional.go.j2 b/src/shacl2code/lang/templates/golang/optional.go.j2 index f7558149..10dec884 100644 --- a/src/shacl2code/lang/templates/golang/optional.go.j2 +++ b/src/shacl2code/lang/templates/golang/optional.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/path.go.j2 b/src/shacl2code/lang/templates/golang/path.go.j2 index 3ca73ce8..cf6ddd6f 100644 --- a/src/shacl2code/lang/templates/golang/path.go.j2 +++ b/src/shacl2code/lang/templates/golang/path.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/property.go.j2 b/src/shacl2code/lang/templates/golang/property.go.j2 index c0225db8..06a08555 100644 --- a/src/shacl2code/lang/templates/golang/property.go.j2 +++ b/src/shacl2code/lang/templates/golang/property.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/ref.go.j2 b/src/shacl2code/lang/templates/golang/ref.go.j2 index a4fe6819..5b918126 100644 --- a/src/shacl2code/lang/templates/golang/ref.go.j2 +++ b/src/shacl2code/lang/templates/golang/ref.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 b/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 index f51482d5..ef969f0b 100644 --- a/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/refproperty.go.j2 b/src/shacl2code/lang/templates/golang/refproperty.go.j2 index 4a32c2d6..53228e83 100644 --- a/src/shacl2code/lang/templates/golang/refproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/refproperty.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/shaclobject.go.j2 b/src/shacl2code/lang/templates/golang/shaclobject.go.j2 index a4e97456..454e4982 100644 --- a/src/shacl2code/lang/templates/golang/shaclobject.go.j2 +++ b/src/shacl2code/lang/templates/golang/shaclobject.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ @@ -21,7 +21,7 @@ type SHACLObjectBase struct { func (self *SHACLObjectBase) ID() PropertyInterface[string] { return &self.id } func (self *SHACLObjectBase) Validate(path Path, handler ErrorHandler) bool { - var valid bool = true + var valid = true switch self.typ.GetNodeKind() { case NodeKindBlankNode: diff --git a/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 b/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 index 397f0741..71d542da 100644 --- a/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 +++ b/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ @@ -173,7 +173,7 @@ func (self *SHACLObjectSetObject) Decode(decoder *json.Decoder) error { } // Check context strings. Using maps.Equal() would be much better, but requires go >= 1.21 - sort.Sort(sort.StringSlice(context_urls)) + sort.Strings(context_urls) if !reflect.DeepEqual(context_urls, CONTEXT_URLS) { return &DecodeError{sub_path, "Wrong context URL(s)"} @@ -304,7 +304,7 @@ func (self *SHACLObjectSetObject) Encode(encoder *json.Encoder) error { // Convert to a list and sort top_list := []SHACLObject{} - for o, _ := range top_objects { + for o := range top_objects { top_list = append(top_list, o) } @@ -348,9 +348,8 @@ func (self *SHACLObjectSetObject) Walk(visit Visit) { visited := map[SHACLObject]bool{} visit_proxy := func(path Path, v any) { - switch v.(type) { + switch r := v.(type) { case Ref[SHACLObject]: - r := v.(Ref[SHACLObject]) if !r.IsObj() { visit(path, v) return diff --git a/src/shacl2code/lang/templates/golang/shacltype.go.j2 b/src/shacl2code/lang/templates/golang/shacltype.go.j2 index ec28064c..3780e0f2 100644 --- a/src/shacl2code/lang/templates/golang/shacltype.go.j2 +++ b/src/shacl2code/lang/templates/golang/shacltype.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/util.go.j2 b/src/shacl2code/lang/templates/golang/util.go.j2 index a1fc9c53..376bb4a6 100644 --- a/src/shacl2code/lang/templates/golang/util.go.j2 +++ b/src/shacl2code/lang/templates/golang/util.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/validator.go.j2 b/src/shacl2code/lang/templates/golang/validator.go.j2 index 35839db2..5e915531 100644 --- a/src/shacl2code/lang/templates/golang/validator.go.j2 +++ b/src/shacl2code/lang/templates/golang/validator.go.j2 @@ -3,7 +3,7 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -{{ disclaimer }} +Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ @@ -22,17 +22,16 @@ type Validator[T any] interface { } func ValueToString(val any, name string) (string, error) { - switch val.(type) { + switch v := val.(type) { case string: - return val.(string), nil + return v, nil case int: - return strconv.Itoa(val.(int)), nil + return strconv.Itoa(v), nil case time.Time: - t := val.(time.Time) - if t.Location() == time.UTC { - return strftime.Format(UtcFormatStr, t), nil + if v.Location() == time.UTC { + return strftime.Format(UtcFormatStr, v), nil } - return strftime.Format(TzFormatStr, t), nil + return strftime.Format(TzFormatStr, v), nil } return "", &ValidationError{name, "Value is of unsupported type " + reflect.TypeOf(val).Name()} } From ad5eb04f6b0b694b5e664ae6fb19f0e8546119b4 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Tue, 7 Jul 2026 22:33:23 +0100 Subject: [PATCH 7/9] Use staticcheck-style comment that use symbol name as prefix Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/lang/templates/golang/classes.go.j2 | 4 ++-- src/shacl2code/lang/templates/golang/errors.go.j2 | 6 +++--- src/shacl2code/lang/templates/golang/ref.go.j2 | 7 ++++--- src/shacl2code/lang/templates/golang/shaclobject.go.j2 | 2 +- src/shacl2code/lang/templates/golang/util.go.j2 | 2 +- src/shacl2code/lang/templates/golang/validator.go.j2 | 10 +++++----- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/shacl2code/lang/templates/golang/classes.go.j2 b/src/shacl2code/lang/templates/golang/classes.go.j2 index 4d245d13..0319e544 100644 --- a/src/shacl2code/lang/templates/golang/classes.go.j2 +++ b/src/shacl2code/lang/templates/golang/classes.go.j2 @@ -21,7 +21,7 @@ import ( // {{ l }} {%- endfor %} {%- else %} -// {{ struct_name(class) }} is a generated SHACL object type +// {{ struct_name(class) }} is a generated SHACL object type. {%- endif %} {%- if class.deprecated %} // @@ -59,7 +59,7 @@ type {{ struct_name(class) }} struct { // {{ l }} {%- endfor %} {% else %} -// {{ constname }} is a generated named individual value +// {{ constname }} is a generated named individual value. {% endif -%} const {{ constname }} = "{{ member._id }}" {%- endfor %} diff --git a/src/shacl2code/lang/templates/golang/errors.go.j2 b/src/shacl2code/lang/templates/golang/errors.go.j2 index 76a99dca..219dabfc 100644 --- a/src/shacl2code/lang/templates/golang/errors.go.j2 +++ b/src/shacl2code/lang/templates/golang/errors.go.j2 @@ -8,7 +8,7 @@ Package {{ package_name }}: {{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ -// Validation Error +// ValidationError reports a SHACL validation failure. type ValidationError struct { Property string Err string @@ -16,7 +16,7 @@ type ValidationError struct { func (e *ValidationError) Error() string { return e.Property + ": " + e.Err } -// Conversion Error +// ConversionError reports a failed reference conversion. type ConversionError struct { From string To string @@ -26,7 +26,7 @@ func (e *ConversionError) Error() string { return "Unable to convert from " + e.From + " to " + e.To } -// Decode Error +// DecodeError reports a SHACL decode failure. type DecodeError struct { Path Path Err string diff --git a/src/shacl2code/lang/templates/golang/ref.go.j2 b/src/shacl2code/lang/templates/golang/ref.go.j2 index 5b918126..0f3f5cf2 100644 --- a/src/shacl2code/lang/templates/golang/ref.go.j2 +++ b/src/shacl2code/lang/templates/golang/ref.go.j2 @@ -12,7 +12,7 @@ import ( "reflect" ) -// Reference +// Ref is a reference to a SHACL object. type Ref[T SHACLObject] interface { GetIRI() string GetObj() T @@ -79,8 +79,9 @@ func MakeIRIRef[T SHACLObject](iri string) Ref[T] { return &ref[T]{nil, iri} } -// Convert one reference to another. Note that the output type is first so it -// can be specified, while the input type is generally inferred from the argument +// ConvertRef converts a reference from one type to another. +// Note that the output type is first so it can be specified, +// while the input type is generally inferred from the argument. func ConvertRef[TO SHACLObject, FROM SHACLObject](in Ref[FROM]) (Ref[TO], error) { if in.IsObj() { out_obj, ok := any(in.GetObj()).(TO) diff --git a/src/shacl2code/lang/templates/golang/shaclobject.go.j2 b/src/shacl2code/lang/templates/golang/shaclobject.go.j2 index 454e4982..0a66e84a 100644 --- a/src/shacl2code/lang/templates/golang/shaclobject.go.j2 +++ b/src/shacl2code/lang/templates/golang/shaclobject.go.j2 @@ -10,7 +10,7 @@ SPDX-License-Identifier: {{ spdx_license }} type Visit func(Path, any) -// Base SHACL Object +// SHACLObjectBase is the base type embedded by all SHACL objects. type SHACLObjectBase struct { // Object ID id Property[string] diff --git a/src/shacl2code/lang/templates/golang/util.go.j2 b/src/shacl2code/lang/templates/golang/util.go.j2 index 376bb4a6..d0e24887 100644 --- a/src/shacl2code/lang/templates/golang/util.go.j2 +++ b/src/shacl2code/lang/templates/golang/util.go.j2 @@ -12,7 +12,7 @@ import ( "strings" ) -// IRI Validation +// IsIRI reports whether iri is a valid IRI, not a blank node. func IsIRI(iri string) bool { if strings.HasPrefix(iri, "_:") { return false diff --git a/src/shacl2code/lang/templates/golang/validator.go.j2 b/src/shacl2code/lang/templates/golang/validator.go.j2 index 5e915531..ed31e832 100644 --- a/src/shacl2code/lang/templates/golang/validator.go.j2 +++ b/src/shacl2code/lang/templates/golang/validator.go.j2 @@ -36,7 +36,7 @@ func ValueToString(val any, name string) (string, error) { return "", &ValidationError{name, "Value is of unsupported type " + reflect.TypeOf(val).Name()} } -// ID Validator +// IDValidator validates that a value is an IRI or a blank node. type IDValidator struct{} func (self IDValidator) Check(val string, name string) error { @@ -46,7 +46,7 @@ func (self IDValidator) Check(val string, name string) error { return nil } -// Regex Validator +// RegexValidator validates a value against a regex pattern. type RegexValidator[T any] struct { Regex string } @@ -67,7 +67,7 @@ func (self RegexValidator[T]) Check(val T, name string) error { return nil } -// Integer Min Validator +// IntegerMinValidator validates that a value is >= a minimum. type IntegerMinValidator struct { Min int } @@ -79,7 +79,7 @@ func (self IntegerMinValidator) Check(val int, name string) error { return nil } -// Integer Max Validator +// IntegerMaxValidator validates that a value is <= a maximum. type IntegerMaxValidator struct { Max int } @@ -91,7 +91,7 @@ func (self IntegerMaxValidator) Check(val int, name string) error { return nil } -// Enum Validator +// EnumValidator validates that a value is one of a fixed set. type EnumValidator struct { Values []string } From 0831f235e0ba4663d9d5027ee19e3629146c0469 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 8 Jul 2026 16:15:16 +0100 Subject: [PATCH 8/9] Put disclaimer in newline Signed-off-by: Arthit Suriyawongkul --- src/shacl2code/lang/templates/golang/classes.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/decode.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/encode.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/errorhandler.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/errors.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/extensible.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/linkstate.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/listproperty.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/optional.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/path.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/property.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/ref.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/reflistproperty.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/refproperty.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/shaclobject.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/shacltype.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/util.go.j2 | 3 ++- src/shacl2code/lang/templates/golang/validator.go.j2 | 3 ++- 19 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/shacl2code/lang/templates/golang/classes.go.j2 b/src/shacl2code/lang/templates/golang/classes.go.j2 index 0319e544..fd30e78a 100644 --- a/src/shacl2code/lang/templates/golang/classes.go.j2 +++ b/src/shacl2code/lang/templates/golang/classes.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/decode.go.j2 b/src/shacl2code/lang/templates/golang/decode.go.j2 index 3925af9d..81f925df 100644 --- a/src/shacl2code/lang/templates/golang/decode.go.j2 +++ b/src/shacl2code/lang/templates/golang/decode.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/encode.go.j2 b/src/shacl2code/lang/templates/golang/encode.go.j2 index e76da450..aa1367f7 100644 --- a/src/shacl2code/lang/templates/golang/encode.go.j2 +++ b/src/shacl2code/lang/templates/golang/encode.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/errorhandler.go.j2 b/src/shacl2code/lang/templates/golang/errorhandler.go.j2 index 856db1c3..eb6e9408 100644 --- a/src/shacl2code/lang/templates/golang/errorhandler.go.j2 +++ b/src/shacl2code/lang/templates/golang/errorhandler.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/errors.go.j2 b/src/shacl2code/lang/templates/golang/errors.go.j2 index 219dabfc..cd4fdaa4 100644 --- a/src/shacl2code/lang/templates/golang/errors.go.j2 +++ b/src/shacl2code/lang/templates/golang/errors.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/extensible.go.j2 b/src/shacl2code/lang/templates/golang/extensible.go.j2 index 0a8d12cf..c21400ca 100644 --- a/src/shacl2code/lang/templates/golang/extensible.go.j2 +++ b/src/shacl2code/lang/templates/golang/extensible.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/linkstate.go.j2 b/src/shacl2code/lang/templates/golang/linkstate.go.j2 index 902954f1..98e6f5d7 100644 --- a/src/shacl2code/lang/templates/golang/linkstate.go.j2 +++ b/src/shacl2code/lang/templates/golang/linkstate.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/listproperty.go.j2 b/src/shacl2code/lang/templates/golang/listproperty.go.j2 index d8bc94af..de557ce2 100644 --- a/src/shacl2code/lang/templates/golang/listproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/listproperty.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/optional.go.j2 b/src/shacl2code/lang/templates/golang/optional.go.j2 index 10dec884..24613768 100644 --- a/src/shacl2code/lang/templates/golang/optional.go.j2 +++ b/src/shacl2code/lang/templates/golang/optional.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/path.go.j2 b/src/shacl2code/lang/templates/golang/path.go.j2 index cf6ddd6f..9c31d1dc 100644 --- a/src/shacl2code/lang/templates/golang/path.go.j2 +++ b/src/shacl2code/lang/templates/golang/path.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/property.go.j2 b/src/shacl2code/lang/templates/golang/property.go.j2 index 06a08555..282f5c43 100644 --- a/src/shacl2code/lang/templates/golang/property.go.j2 +++ b/src/shacl2code/lang/templates/golang/property.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/ref.go.j2 b/src/shacl2code/lang/templates/golang/ref.go.j2 index 0f3f5cf2..445ce20a 100644 --- a/src/shacl2code/lang/templates/golang/ref.go.j2 +++ b/src/shacl2code/lang/templates/golang/ref.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 b/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 index ef969f0b..c4ca4130 100644 --- a/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/reflistproperty.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/refproperty.go.j2 b/src/shacl2code/lang/templates/golang/refproperty.go.j2 index 53228e83..a5486ba8 100644 --- a/src/shacl2code/lang/templates/golang/refproperty.go.j2 +++ b/src/shacl2code/lang/templates/golang/refproperty.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/shaclobject.go.j2 b/src/shacl2code/lang/templates/golang/shaclobject.go.j2 index 0a66e84a..aff78310 100644 --- a/src/shacl2code/lang/templates/golang/shaclobject.go.j2 +++ b/src/shacl2code/lang/templates/golang/shaclobject.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 b/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 index 71d542da..b080e03a 100644 --- a/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 +++ b/src/shacl2code/lang/templates/golang/shaclobjectset.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/shacltype.go.j2 b/src/shacl2code/lang/templates/golang/shacltype.go.j2 index 3780e0f2..dffa6cd3 100644 --- a/src/shacl2code/lang/templates/golang/shacltype.go.j2 +++ b/src/shacl2code/lang/templates/golang/shacltype.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/util.go.j2 b/src/shacl2code/lang/templates/golang/util.go.j2 index d0e24887..ac7deed5 100644 --- a/src/shacl2code/lang/templates/golang/util.go.j2 +++ b/src/shacl2code/lang/templates/golang/util.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ diff --git a/src/shacl2code/lang/templates/golang/validator.go.j2 b/src/shacl2code/lang/templates/golang/validator.go.j2 index ed31e832..f908a2bd 100644 --- a/src/shacl2code/lang/templates/golang/validator.go.j2 +++ b/src/shacl2code/lang/templates/golang/validator.go.j2 @@ -3,7 +3,8 @@ vim: ft=go */ // This line keeps gofmt happy package shacl2code /* #} -Package {{ package_name }}: {{ disclaimer }} +Package {{ package_name }}: +{{ disclaimer }} SPDX-License-Identifier: {{ spdx_license }} {{ go_package }} */ From 8ffff36cbc6bb487a54fa87d842d94ef2ad5ddcb Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Wed, 8 Jul 2026 18:51:14 +0100 Subject: [PATCH 9/9] Do not use pyshacl 0.40.0 with Python 3.9 Signed-off-by: Arthit Suriyawongkul --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e58cfe30..22676237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dev = [ "mypy >= 1.19.1", # latest version for Python 3.9 is 1.19.x "pyrefly >= 0.55.0", "pyright >= 1.1.403", - "pyshacl >= 0.25.0", + "pyshacl >= 0.25.0, < 0.40.0", # 0.40.0 uses `str | X` at runtime, breaks on Python < 3.10 "pytest >= 7.4", "pytest-cov >= 4.1", "pytest-xdist >= 3.5",