Repository: pypa/setuptools_scm

Issue:
Using a custom version_scheme outside setup.py
setuptools_scm allows to use a custom version_scheme function. When not using `setup.py` this needs to be an entry-point name. However, this doesn't work logically. When the project is installed (e.g. `pip install -e .`), setuptools asks setuptools_scm for the version tp install the project. But the project is not installed at that point and therefore the entry point is not accessible. Then, the install will fail due to `assert main_version is not None`.
Can you please check how to solve this chicken-egg issue?

One idea - Support qualified names: Instead of an entry-point give the qualified name to a function and setuptools_scm will execute it


Discussion:

Patch:
diff --git a/README.rst b/README.rst
index fd2f18c..8914fc8 100644
--- a/README.rst
+++ b/README.rst
@@ -628,6 +628,25 @@ The callable must return the configuration.
     )
 
 
+Customizing Version Scheme with pyproject.toml
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To support custom version schemes in pyproject.toml, you may specify your own function as an entrypoint for getting the version.
+
+.. code:: toml
+
+    # pyproject.toml
+    [tool.setuptools_scm]
+    version_scheme = "myproject.my_file:myversion_func"
+
+.. code:: python
+
+    # myproject/my_file
+    def myversion_func(version: ScmVersion):
+        from setuptools_scm.version import guess_next_version
+        return version.format_next_version(guess_next_version, '{guessed}b{distance}')
+
+
 Note on testing non-installed versions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py
index c236434..0b0b32c 100644
--- a/src/setuptools_scm/_entrypoints.py
+++ b/src/setuptools_scm/_entrypoints.py
@@ -46,9 +46,11 @@ def _version_from_entrypoints(
 
 try:
     from importlib.metadata import entry_points  # type: ignore
+    from importlib.metadata import EntryPoint
 except ImportError:
     try:
         from importlib_metadata import entry_points
+        from importlib_metadata import EntryPoint
     except ImportError:
         from collections import defaultdict
 
@@ -59,6 +61,10 @@ except ImportError:
             )
             return defaultdict(list)
 
+        class EntryPoint:  # type: ignore
+            def __init__(self, *args: Any, **kwargs: Any):
+                pass  # entry_points() already provides the warning
+
 
 def iter_entry_points(
     group: str, name: str | None = None
@@ -83,6 +89,13 @@ def _get_ep(group: str, name: str) -> Any | None:
         return None
 
 
+def _get_from_object_reference_str(path: str) -> Any | None:
+    try:
+        return EntryPoint(path, path, None).load()
+    except (AttributeError, ModuleNotFoundError):
+        return None
+
+
 def _iter_version_schemes(
     entrypoint: str,
     scheme_value: _t.VERSION_SCHEMES,
@@ -93,7 +106,8 @@ def _iter_version_schemes(
     if isinstance(scheme_value, str):
         scheme_value = cast(
             "_t.VERSION_SCHEMES",
-            _get_ep(entrypoint, scheme_value),
+            _get_ep(entrypoint, scheme_value)
+            or _get_from_object_reference_str(scheme_value),
         )
 
     if isinstance(scheme_value, (list, tuple)):


Test patch:
diff --git a/testing/test_version.py b/testing/test_version.py
index 7c68d42..778b0c2 100644
--- a/testing/test_version.py
+++ b/testing/test_version.py
@@ -214,6 +214,16 @@ def test_format_version_schemes() -> None:
     )
 
 
+def test_custom_version_schemes() -> None:
+    version = meta("1.0", config=c)
+    custom_computed = format_version(
+        version,
+        local_scheme="no-local-version",
+        version_scheme="setuptools_scm.version:no_guess_dev_version",
+    )
+    assert custom_computed == no_guess_dev_version(version)
+
+
 def date_to_str(
     date_: date | None = None,
     days_offset: int = 0,