Repository: OpenAssetIO/OpenAssetIO (language: python)

Issue:
Library version introspection mechanism
## What

Ensure at exact version can be determined at build time and run time

## Why
- Traitgen/Mediacreation versioning needs openassetio version information for probable implementation strategies.
  - Deprecations
- Would be useful for the plugin system to know what version of openassetio they're in
- Potentially useful for pipeline engineers setting up pipelines.

## ACs
- [ ] A runtime method or property returning the openassetio version (C++ / Python)
- [ ] Major version, minor version, patch version and prelease returned in runtime accesor somehow
- [ ] Runtime accessor that returns a version string
- [ ] A compile time method of introspection such as a version header
- [ ] Can access in macros, Major version, minor version, patch version and prelease from compile time mechanism
- [ ] Version info should  require minimal manual updates

## Out of scope
- Python does not need a "compile time" equivilant version mechanism
Library version introspection mechanism
## What

Ensure at exact version can be determined at build time and run time

## Why
- Traitgen/Mediacreation versioning needs openassetio version information for probable implementation strategies.
  - Deprecations
- Would be useful for the plugin system to know what version of openassetio they're in
- Potentially useful for pipeline engineers setting up pipelines.

## ACs
- [ ] A runtime method or property returning the openassetio version (C++ / Python)
- [ ] Major version, minor version, patch version and prelease returned in runtime accesor somehow
- [ ] Runtime accessor that returns a version string
- [ ] A compile time method of introspection such as a version header
- [ ] Can access in macros, Major version, minor version, patch version and prelease from compile time mechanism
- [ ] Version info should  require minimal manual updates

## Out of scope
- Python does not need a "compile time" equivilant version mechanism


Discussion / hints:
As with all things, there is a PEP for this: https://peps.python.org/pep-0396/.  TLDR we add a top-level `__version__` attribute.
As with all things, there is a PEP for this: https://peps.python.org/pep-0396/.  TLDR we add a top-level `__version__` attribute.

Fix patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 9bf69ee2a..1c4ca4a27 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -24,6 +24,11 @@ v1.0.0-beta.x.x
   when transparently called from C++.
   [#947](https://github.com/OpenAssetIO/OpenAssetIO/issues/947)
 
+- Add version accessor methods to allow runtime introspection of
+  OpenAssetIO library version. `version.h` header distributed with
+  install to allow compile time introspection.
+  [#1173](https://github.com/OpenAssetIO/OpenAssetIO/issues/1173)
+
 ### Improvements
 
 - Added the string representation of an `EntityReference` when `repr`'d
diff --git a/cmake/templates/include/openassetio/version.hpp.in b/cmake/templates/include/openassetio/version.hpp.in
new file mode 100644
index 000000000..36dde22c6
--- /dev/null
+++ b/cmake/templates/include/openassetio/version.hpp.in
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 The Foundry Visionmongers Ltd
+#pragma once
+
+#include <cstddef>
+#include <string_view>
+
+#include <openassetio/export.h>
+
+// clang-format off
+#define OPENASSETIO_VERSION_MAJOR @OpenAssetIO_VERSION_MAJOR@
+#define OPENASSETIO_VERSION_MINOR @OpenAssetIO_VERSION_MINOR@
+#define OPENASSETIO_VERSION_PATCH @OpenAssetIO_VERSION_PATCH@
+// clang-format on
+
+// For the time being, beta versions must be manually updated prior to
+// the release
+#define OPENASSETIO_BETA_VERSION_MAJOR 1
+#define OPENASSETIO_BETA_VERSION_MINOR 0
+
+#define OPENASSETIO_STRINGIFY(x) #x
+#define OPENASSETIO_MACRO_TO_STRING(x) OPENASSETIO_STRINGIFY(x)
+
+// clang-format off
+#define OPENASSETIO_VERSION_STRING                                                             \
+  "v@OpenAssetIO_VERSION@-beta." OPENASSETIO_MACRO_TO_STRING(OPENASSETIO_BETA_VERSION_MAJOR)\
+  "." OPENASSETIO_MACRO_TO_STRING(OPENASSETIO_BETA_VERSION_MINOR)
+// clang-format on
+
+namespace openassetio {
+inline namespace OPENASSETIO_CORE_ABI_VERSION {
+/**
+ * The major version of the OpenAssetIO library.
+ */
+constexpr std::size_t majorVersion() { return OPENASSETIO_VERSION_MAJOR; }
+/**
+ * The minor version of the OpenAssetIO library.
+ */
+constexpr std::size_t minorVersion() { return OPENASSETIO_VERSION_MINOR; }
+/**
+ * The patch version of the OpenAssetIO library.
+ */
+constexpr std::size_t patchVersion() { return OPENASSETIO_VERSION_PATCH; }
+
+/**
+ * The beta major version of the OpenAssetIO library.
+ *
+ * Whilst OpenAssetIO is in beta, its main versioning remains at 1.0.0,
+ * and the beta versions are incremented.
+ * When OpenAssetIO leaves beta, this value will return 0.
+ */
+constexpr std::size_t betaMajorVersion() { return OPENASSETIO_BETA_VERSION_MAJOR; }
+/**
+ * The beta minor version of the OpenAssetIO library.
+ *
+ * Whilst OpenAssetIO is in beta, its main versioning remains at 1.0.0,
+ * and the beta versions are incremented.
+ * When OpenAssetIO leaves beta, this value will return 0.
+ */
+constexpr std::size_t betaMinorVersion() { return OPENASSETIO_BETA_VERSION_MINOR; }
+
+/**
+ * A string containing the OpenAssetIO version string.
+ *
+ * The format is {MAJOR}.{MINOR}.{PATCH}-beta{BETAMAJOR}.{BETAMINOR}.
+ * When OpenAssetIO leaves beta, the beta section will be removed.
+ */
+constexpr std::string_view versionString() { return OPENASSETIO_VERSION_STRING; }
+}  // namespace OPENASSETIO_CORE_ABI_VERSION
+}  // namespace openassetio
\ No newline at end of file
diff --git a/doc/contributing/PROCESS.md b/doc/contributing/PROCESS.md
index a4021c169..952d15335 100644
--- a/doc/contributing/PROCESS.md
+++ b/doc/contributing/PROCESS.md
@@ -130,6 +130,7 @@ To make a new OpenAssetIO release, follow this procedure.
   to v1.0.0-alpha.5).
 - Update version numbers in :
   - [pyproject.toml](../../pyproject.toml) (Under `[project]` section).
+  - [test_version.py](../../src/openassetio-python/tests/package/test_version.py) (At the top).
   - [CMakeLists.txt](../../CMakeLists.txt) (An argument to `project()`,
       no need to update for an alpha or beta increment).
 - If there is an ABI change (major release), update
diff --git a/src/openassetio-core/CMakeLists.txt b/src/openassetio-core/CMakeLists.txt
index 6ed1f6b65..31b38c41f 100644
--- a/src/openassetio-core/CMakeLists.txt
+++ b/src/openassetio-core/CMakeLists.txt
@@ -1,5 +1,5 @@
 # SPDX-License-Identifier: Apache-2.0
-# Copyright 2013-2022 The Foundry Visionmongers Ltd
+# Copyright 2013-2024 The Foundry Visionmongers Ltd
 
 #----------------------------------------------------------------------
 # Versioning
@@ -85,7 +85,7 @@ target_sources(
 # Public header dependency.
 target_include_directories(openassetio-core
     PUBLIC
-    # For generated export.h header.
+    # For generated export.h and version.hpp header.
     "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>"
     # Use includes from source tree for building.
     "$<BUILD_INTERFACE:${_public_header_source_root}>"
@@ -126,6 +126,16 @@ install(
     DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/openassetio/
 )
 
+#----------------------------------------------------------------------
+# Version header
+# Create a version.hpp with the configured version nums from template
+configure_file(${PROJECT_SOURCE_DIR}/cmake/templates/include/openassetio/version.hpp.in
+               ${PROJECT_BINARY_DIR}/include/openassetio/version.hpp @ONLY)
+
+install(
+    FILES ${PROJECT_BINARY_DIR}/include/openassetio/version.hpp
+    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/openassetio/
+)
 
 #-----------------------------------------------------------------------
 # Tests
diff --git a/src/openassetio-python/cmodule/CMakeLists.txt b/src/openassetio-python/cmodule/CMakeLists.txt
index ffa7a6066..678c16ff0 100644
--- a/src/openassetio-python/cmodule/CMakeLists.txt
+++ b/src/openassetio-python/cmodule/CMakeLists.txt
@@ -45,6 +45,7 @@ target_sources(
     src/constantsBinding.cpp
     src/ContextBinding.cpp
     src/EntityReferenceBinding.cpp
+    src/versionBinding.cpp
     src/errors/exceptionsAsserts.cpp
     src/errors/exceptionsBinding.cpp
     src/errors/BatchElementErrorBinding.cpp
diff --git a/src/openassetio-python/cmodule/src/_openassetio.cpp b/src/openassetio-python/cmodule/src/_openassetio.cpp
index 0d32e58a4..5a255d1e1 100644
--- a/src/openassetio-python/cmodule/src/_openassetio.cpp
+++ b/src/openassetio-python/cmodule/src/_openassetio.cpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: Apache-2.0
-// Copyright 2013-2023 The Foundry Visionmongers Ltd
+// Copyright 2013-2024 The Foundry Visionmongers Ltd
 
 #include "_openassetio.hpp"
 
@@ -20,6 +20,7 @@ PYBIND11_MODULE(_openassetio, mod) {
   const py::module errors = mod.def_submodule("errors");
   const py::module trait = mod.def_submodule("trait");
 
+  registerVersion(mod);
   registerAccess(access);
   registerConstants(constants);
   registerLoggerInterface(log);
diff --git a/src/openassetio-python/cmodule/src/_openassetio.hpp b/src/openassetio-python/cmodule/src/_openassetio.hpp
index e4bf563ca..8022a1cf1 100644
--- a/src/openassetio-python/cmodule/src/_openassetio.hpp
+++ b/src/openassetio-python/cmodule/src/_openassetio.hpp
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: Apache-2.0
-// Copyright 2013-2023 The Foundry Visionmongers Ltd
+// Copyright 2013-2024 The Foundry Visionmongers Ltd
 /**
  * Python binding bootstrap functions and typedefs.
  */
@@ -43,6 +43,9 @@ using RetainCommonPyArgs = openassetio::RetainPyArgs<
 /// Concise pybind alias.
 namespace py = pybind11;
 
+// Register version introspection methods
+void registerVersion(py::module& mod);
+
 /// Register access enums and strings.
 void registerAccess(const py::module& mod);
 
diff --git a/src/openassetio-python/cmodule/src/versionBinding.cpp b/src/openassetio-python/cmodule/src/versionBinding.cpp
new file mode 100644
index 000000000..3e36d44bf
--- /dev/null
+++ b/src/openassetio-python/cmodule/src/versionBinding.cpp
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 The Foundry Visionmongers Ltd
+#include <pybind11/pybind11.h>
+
+#include <openassetio/version.hpp>
+#include "_openassetio.hpp"
+
+void registerVersion(py::module& mod) {
+  mod.def("majorVersion", &openassetio::majorVersion);
+  mod.def("minorVersion", &openassetio::minorVersion);
+  mod.def("patchVersion", &openassetio::patchVersion);
+  mod.def("betaMajorVersion", &openassetio::betaMajorVersion);
+  mod.def("betaMinorVersion", &openassetio::betaMinorVersion);
+  mod.def("versionString", &openassetio::versionString);
+}
diff --git a/src/openassetio-python/package/openassetio/__init__.py b/src/openassetio-python/package/openassetio/__init__.py
index fb585d612..1a5f33e30 100644
--- a/src/openassetio-python/package/openassetio/__init__.py
+++ b/src/openassetio-python/package/openassetio/__init__.py
@@ -1,5 +1,5 @@
 #
-#   Copyright 2013-2021 The Foundry Visionmongers Ltd
+#   Copyright 2013-2024 The Foundry Visionmongers Ltd
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
 #   you may not use this file except in compliance with the License.
@@ -73,6 +73,12 @@
     constants,
     Context,
     EntityReference,
+    majorVersion,
+    minorVersion,
+    patchVersion,
+    betaMajorVersion,
+    betaMinorVersion,
+    versionString,
 )
 
 


Test patch:
diff --git a/src/openassetio-core/tests/CMakeLists.txt b/src/openassetio-core/tests/CMakeLists.txt
index 8f1cbf7e0..f5277cf41 100644
--- a/src/openassetio-core/tests/CMakeLists.txt
+++ b/src/openassetio-core/tests/CMakeLists.txt
@@ -22,6 +22,7 @@ target_sources(openassetio-core-cpp-test-exe
     ContextTest.cpp
     TraitsDataTest.cpp
     deprecationsTest.cpp
+    versionTest.cpp
     hostApi/ManagerTest.cpp
     managerApi/HostTest.cpp
     managerApi/HostSessionTest.cpp
diff --git a/src/openassetio-core/tests/versionTest.cpp b/src/openassetio-core/tests/versionTest.cpp
new file mode 100644
index 000000000..5e029cfd0
--- /dev/null
+++ b/src/openassetio-core/tests/versionTest.cpp
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2024 The Foundry Visionmongers Ltd
+#include <catch2/catch.hpp>
+
+#include <openassetio/version.hpp>
+
+SCENARIO("OpenassetIO exposes version information") {
+  CHECK(openassetio::majorVersion() == OPENASSETIO_VERSION_MAJOR);
+  CHECK(openassetio::minorVersion() == OPENASSETIO_VERSION_MINOR);
+  CHECK(openassetio::patchVersion() == OPENASSETIO_VERSION_PATCH);
+  CHECK(openassetio::betaMajorVersion() == OPENASSETIO_BETA_VERSION_MAJOR);
+  CHECK(openassetio::betaMinorVersion() == OPENASSETIO_BETA_VERSION_MINOR);
+  CHECK(openassetio::versionString() == OPENASSETIO_VERSION_STRING);
+}
diff --git a/src/openassetio-python/tests/package/test_version.py b/src/openassetio-python/tests/package/test_version.py
new file mode 100644
index 000000000..f6b9305af
--- /dev/null
+++ b/src/openassetio-python/tests/package/test_version.py
@@ -0,0 +1,70 @@
+#
+# Copyright 2024 The Foundry Visionmongers Ltd
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""
+Tests that cover the version introspection methods
+"""
+
+# pylint: disable=invalid-name,redefined-outer-name
+# pylint: disable=too-few-public-methods
+# pylint: disable=missing-class-docstring,missing-function-docstring
+
+import re
+
+import pytest
+
+import openassetio
+
+# Update this value each release
+openassetio_version_string = "v1.0.0-beta.1.0"
+
+
+@pytest.fixture(scope="module")
+def extracted_version_nums():
+    # Extract version numbers so we can test them
+    pattern = r"v(\d+)\.(\d+)\.(\d+)-beta\.(\d+)\.(\d+)"
+    match = re.search(pattern, openassetio_version_string)
+    major_version = match.group(1)
+    minor_version = match.group(2)
+    patch_version = match.group(3)
+    beta_major_version = match.group(4)
+    beta_minor_version = match.group(5)
+    return (
+        int(major_version),
+        int(minor_version),
+        int(patch_version),
+        int(beta_major_version),
+        int(beta_minor_version),
+    )
+
+
+class Test_Version:
+    def test_version_string(self):
+        assert openassetio.versionString() == openassetio_version_string
+
+    def test_major_version(self, extracted_version_nums):
+        assert openassetio.majorVersion() == extracted_version_nums[0]
+
+    def test_minor_version(self, extracted_version_nums):
+        assert openassetio.minorVersion() == extracted_version_nums[1]
+
+    def test_patch_version(self, extracted_version_nums):
+        assert openassetio.patchVersion() == extracted_version_nums[2]
+
+    def test_major_beta_version(self, extracted_version_nums):
+        assert openassetio.betaMajorVersion() == extracted_version_nums[3]
+
+    def test_minor_beta_version(self, extracted_version_nums):
+        assert openassetio.betaMinorVersion() == extracted_version_nums[4]