Skip to content

Extending setuptools-scm

setuptools-scm uses entry-point based hooks to extend its default capabilities.

Adding a new SCM

setuptools-scm provides two entrypoints for adding new SCMs:

setuptools_scm.parse_scm

A function used to parse the metadata of the current workdir using the name of the control directory/file of your SCM as the entrypoint's name. E.g. for the built-in entrypoint for Git the entrypoint is named .git and references setuptools_scm.git:parse

The return value MUST be a vcs_versioning.ScmVersion instance created by the function vcs_versioning._version_schemes.meta.

setuptools_scm.files_command
Either a string containing a shell command that prints all SCM-managed files in its current working directory or a callable, that given a pathname will return that list.

Also uses the name of your SCM control directory as the name of the entrypoint.

api reference for scm version objects

vcs_versioning.ScmVersion dataclass

represents a parsed version from scm

Source code in vcs-versioning/src/vcs_versioning/_scm_version.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
@dataclasses.dataclass
class ScmVersion:
    """represents a parsed version from scm"""

    tag: _v.Version | _v.NonNormalizedVersion
    """the related tag or preformatted version"""
    config: _config.Configuration
    """the configuration used to parse the version"""
    distance: int = 0
    """the number of commits since the tag"""
    node: str | None = None
    """the shortened node id"""
    dirty: bool = False
    """whether the working copy had uncommitted changes"""
    preformatted: bool = False
    """whether the version string was preformatted"""
    branch: str | None = None
    """the branch name if any"""
    node_date: date | None = None
    """the date of the commit if available"""
    time: datetime = dataclasses.field(default_factory=_source_epoch_or_utc_now)
    """the current time or source epoch time
    only set for unit-testing version schemes
    for real usage it must be `now(utc)` or `SOURCE_EPOCH`
    """

    @property
    def exact(self) -> bool:
        """returns true checked out exactly on a tag and no local changes apply"""
        return self.distance == 0 and not self.dirty

    @property
    def short_node(self) -> str | None:
        """Return the node formatted for output."""
        return _format_node_for_output(self.node)

    def __repr__(self) -> str:
        return (
            f"<ScmVersion {self.tag} dist={self.distance} "
            f"node={self.node} dirty={self.dirty} branch={self.branch}>"
        )

    def format_with(self, fmt: str, **kw: object) -> str:
        """format a given format string with attributes of this object"""
        return fmt.format(
            time=self.time,
            tag=self.tag,
            distance=self.distance,
            node=_format_node_for_output(self.node),
            dirty=self.dirty,
            branch=self.branch,
            node_date=self.node_date,
            **kw,
        )

    def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str:
        """given `clean_format` and `dirty_format`

        choose one based on `self.dirty` and format it using `self.format_with`"""

        return self.format_with(dirty_format if self.dirty else clean_format, **kw)

    def format_next_version(
        self,
        guess_next: Callable[Concatenate[ScmVersion, _P], str],
        fmt: str = "{guessed}.dev{distance}",
        *k: _P.args,
        **kw: _P.kwargs,
    ) -> str:
        guessed = guess_next(self, *k, **kw)
        return self.format_with(fmt, guessed=guessed)

    def matches(self, **expectations: Unpack[VersionExpectations]) -> bool | mismatches:
        """Check if this ScmVersion matches the given expectations.

        Returns True if all specified properties match, or a mismatches
        object (which is falsy) containing details of what didn't match.

        Args:
            **expectations: Properties to check, using VersionExpectations TypedDict
        """
        # Map expectation keys to ScmVersion attributes
        attr_map: dict[str, Callable[[], Any]] = {
            "tag": lambda: str(self.tag),
            "node_prefix": lambda: self.node,
            "distance": lambda: self.distance,
            "dirty": lambda: self.dirty,
            "branch": lambda: self.branch,
            "exact": lambda: self.exact,
            "preformatted": lambda: self.preformatted,
            "node_date": lambda: self.node_date,
            "time": lambda: self.time,
        }

        # Build actual values dict
        actual: dict[str, Any] = {
            key: attr_map[key]() for key in expectations if key in attr_map
        }

        # Process expectations
        expected = {
            "tag" if k == "tag" else k: str(v) if k == "tag" else v
            for k, v in expectations.items()
        }

        # Check for mismatches
        def has_mismatch() -> bool:
            for key, exp_val in expected.items():
                if key == "node_prefix":
                    act_val = actual.get("node_prefix")
                    if not act_val or not act_val.startswith(exp_val):
                        return True
                else:
                    if str(exp_val) != str(actual.get(key)):
                        return True
            return False

        if has_mismatch():
            # Rename node_prefix back to node for actual values in mismatch reporting
            if "node_prefix" in actual:
                actual["node"] = actual.pop("node_prefix")
            return mismatches(expected=expected, actual=actual)
        return True
branch class-attribute instance-attribute
branch: str | None = None

the branch name if any

config instance-attribute
config: Configuration

the configuration used to parse the version

dirty class-attribute instance-attribute
dirty: bool = False

whether the working copy had uncommitted changes

distance class-attribute instance-attribute
distance: int = 0

the number of commits since the tag

exact property
exact: bool

returns true checked out exactly on a tag and no local changes apply

node class-attribute instance-attribute
node: str | None = None

the shortened node id

node_date class-attribute instance-attribute
node_date: date | None = None

the date of the commit if available

preformatted class-attribute instance-attribute
preformatted: bool = False

whether the version string was preformatted

short_node property
short_node: str | None

Return the node formatted for output.

tag instance-attribute
tag: Version | NonNormalizedVersion

the related tag or preformatted version

time class-attribute instance-attribute
time: datetime = field(default_factory=_source_epoch_or_utc_now)

the current time or source epoch time only set for unit-testing version schemes for real usage it must be now(utc) or SOURCE_EPOCH

format_choice
format_choice(clean_format: str, dirty_format: str, **kw: object) -> str

given clean_format and dirty_format

choose one based on self.dirty and format it using self.format_with

Source code in vcs-versioning/src/vcs_versioning/_scm_version.py
245
246
247
248
249
250
def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str:
    """given `clean_format` and `dirty_format`

    choose one based on `self.dirty` and format it using `self.format_with`"""

    return self.format_with(dirty_format if self.dirty else clean_format, **kw)
format_with
format_with(fmt: str, **kw: object) -> str

format a given format string with attributes of this object

Source code in vcs-versioning/src/vcs_versioning/_scm_version.py
232
233
234
235
236
237
238
239
240
241
242
243
def format_with(self, fmt: str, **kw: object) -> str:
    """format a given format string with attributes of this object"""
    return fmt.format(
        time=self.time,
        tag=self.tag,
        distance=self.distance,
        node=_format_node_for_output(self.node),
        dirty=self.dirty,
        branch=self.branch,
        node_date=self.node_date,
        **kw,
    )
matches
matches(**expectations: Unpack[VersionExpectations]) -> bool | mismatches

Check if this ScmVersion matches the given expectations.

Returns True if all specified properties match, or a mismatches object (which is falsy) containing details of what didn't match.

Parameters:

Name Type Description Default
**expectations Unpack[VersionExpectations]

Properties to check, using VersionExpectations TypedDict

{}
Source code in vcs-versioning/src/vcs_versioning/_scm_version.py
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
def matches(self, **expectations: Unpack[VersionExpectations]) -> bool | mismatches:
    """Check if this ScmVersion matches the given expectations.

    Returns True if all specified properties match, or a mismatches
    object (which is falsy) containing details of what didn't match.

    Args:
        **expectations: Properties to check, using VersionExpectations TypedDict
    """
    # Map expectation keys to ScmVersion attributes
    attr_map: dict[str, Callable[[], Any]] = {
        "tag": lambda: str(self.tag),
        "node_prefix": lambda: self.node,
        "distance": lambda: self.distance,
        "dirty": lambda: self.dirty,
        "branch": lambda: self.branch,
        "exact": lambda: self.exact,
        "preformatted": lambda: self.preformatted,
        "node_date": lambda: self.node_date,
        "time": lambda: self.time,
    }

    # Build actual values dict
    actual: dict[str, Any] = {
        key: attr_map[key]() for key in expectations if key in attr_map
    }

    # Process expectations
    expected = {
        "tag" if k == "tag" else k: str(v) if k == "tag" else v
        for k, v in expectations.items()
    }

    # Check for mismatches
    def has_mismatch() -> bool:
        for key, exp_val in expected.items():
            if key == "node_prefix":
                act_val = actual.get("node_prefix")
                if not act_val or not act_val.startswith(exp_val):
                    return True
            else:
                if str(exp_val) != str(actual.get(key)):
                    return True
        return False

    if has_mismatch():
        # Rename node_prefix back to node for actual values in mismatch reporting
        if "node_prefix" in actual:
            actual["node"] = actual.pop("node_prefix")
        return mismatches(expected=expected, actual=actual)
    return True

vcs_versioning._version_schemes.meta

meta(tag: str | _Version, *, distance: int = 0, dirty: bool = False, node: str | None = None, preformatted: bool = False, branch: str | None = None, config: Configuration, node_date: date | None = None, time: datetime | None = None) -> ScmVersion
Source code in vcs-versioning/src/vcs_versioning/_scm_version.py
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def meta(
    tag: str | _Version,
    *,
    distance: int = 0,
    dirty: bool = False,
    node: str | None = None,
    preformatted: bool = False,
    branch: str | None = None,
    config: _config.Configuration,
    node_date: date | None = None,
    time: datetime | None = None,
) -> ScmVersion:
    parsed_version: _Version
    # Enhanced string validation for preformatted versions
    if preformatted and isinstance(tag, str):
        # Validate PEP 440 compliance using NonNormalizedVersion
        # Let validation errors bubble up to the caller
        parsed_version = _v.NonNormalizedVersion(tag)
    else:
        # Use existing _parse_tag logic for non-preformatted or already validated inputs
        parsed_version = _parse_tag(tag, preformatted, config)

    log.info("version %s -> %s", tag, parsed_version)
    assert parsed_version is not None, f"Can't parse version {tag}"

    # Pass time explicitly to avoid triggering default_factory if provided
    kwargs: _ScmVersionKwargs = {
        "distance": distance,
        "node": node,
        "dirty": dirty,
        "preformatted": preformatted,
        "branch": branch,
        "node_date": node_date,
    }
    if time is not None:
        kwargs["time"] = time

    scm_version = ScmVersion(parsed_version, config=config, **kwargs)
    return scm_version

vcs_versioning._version_schemes.DirtyWorkingTreeError

Bases: ValueError

Raised when the fail-on-uncommitted-changes local scheme sees a dirty tree.

Source code in vcs-versioning/src/vcs_versioning/_exceptions.py
4
5
class DirtyWorkingTreeError(ValueError):
    """Raised when the ``fail-on-uncommitted-changes`` local scheme sees a dirty tree."""

Version number construction

setuptools_scm.version_scheme

Configures how the version number is constructed given a ScmVersion instance and should return a string representing the version.

Available implementations

guess-next-dev (default)

Automatically guesses the next development version (default). Guesses the upcoming release by incrementing the pre-release segment if present, otherwise by incrementing the micro segment. Then appends .devN. In case the tag ends with .dev0 the version is not bumped and custom .devN versions will trigger an error.

Examples:

  • Tag v1.0.0 → version 1.0.1.dev0 (if dirty or distance > 0)
  • Tag v1.0.0 → version 1.0.0 (if exact match)
calver-by-date

Calendar versioning scheme that generates versions based on dates. Uses the format YY.MM.DD.patch or YYYY.MM.DD.patch depending on the existing tag format. If the commit is on the same date as the latest tag, increments the patch number. Otherwise, uses the current date with patch 0. Supports branch-specific versioning for release branches.

Examples:

  • Tag v23.01.15.0 on same day → version 23.01.15.1.devN
  • Tag v23.01.15.0 on different day (e.g., 2023-01-16) → version 23.01.16.0.devN
  • Tag v2023.01.15.0 → uses 4-digit year format for new versions
no-guess-dev

Does no next version guessing, just adds .post1.devN. This is the recommended replacement for the deprecated post-release scheme.

Examples:

  • Tag v1.0.0 → version 1.0.0.post1.devN (if distance > 0)
  • Tag v1.0.0 → version 1.0.0 (if exact match)
only-version

Only use the version from the tag, as given.

This means version is no longer pseudo unique per commit

Examples:

  • Tag v1.0.0 → version 1.0.0 (always, regardless of distance or dirty state)
post-release (deprecated)

Generates post release versions (adds .postN) after review of the version number pep this is considered a bad idea as post releases are intended to be chosen not autogenerated.

the recommended replacement is no-guess-dev

Examples:

  • Tag 1.0.0 → version 1.0.0.postN (where N is the distance)
semver-pep440

Basic semantic versioning with PEP 440-compliant version numbers.

Guesses the upcoming release by incrementing the minor segment and setting the micro segment to zero if the current branch contains the string feature, otherwise by incrementing the micro version. Then appending .devN.

This scheme is not compatible with pre-releases.

Renamed in setuptools-scm 10

Previously called python-simplified-semver. The old name still works but is deprecated.

Examples:

  • Tag 1.0.0 on non-feature branch → version 1.0.1.devN
  • Tag 1.0.0 on feature branch → version 1.1.0.devN
semver-pep440-release-branch

Semantic versioning with PEP 440-compliant version numbers for projects with release branches. The same as guess-next-dev (incrementing the pre-release or micro segment) however when on a release branch: a branch whose name (ignoring namespace) parses as a version that matches the most recent tag up to the minor segment. Otherwise if on a non-release branch, increments the minor segment and sets the micro segment to zero, then appends .devN

Namespaces are unix pathname separated parts of a branch/tag name.

Renamed in setuptools-scm 10

Previously called release-branch-semver. The old name still works but is deprecated.

Examples:

  • Tag 1.0.0 on release branch release-1.0 → version 1.0.1.devN

  • Tag 1.0.0 on release branch release/v1.0 → version 1.0.1.devN

  • Tag 1.0.0 on development branch → version 1.1.0.devN

setuptools_scm.local_scheme

Configures how the local part of a version is rendered given a ScmVersion instance. A callable should return a string for the local segment, or you may pass a list of scheme names—each is tried in order until one returns a non-None value (an empty string still stops the chain; return None to defer to the next scheme—see fail-on-uncommitted-changes below). Dates and times are in Coordinated Universal Time (UTC), because as part of the version, they should be location independent.

Available implementations

node-and-date (default)
Adds the node on dev versions and the date on dirty workdir
node-and-timestamp
Like node-and-date but with a timestamp of the form %Y%m%d%H%M%S instead
dirty-tag
Adds +dirty if the current workdir has changes
no-local-version
Omits local version, useful e.g. because PyPI does not support it
no-local-version-strict
Like no-local-version but raises DirtyWorkingTreeError when the working tree is dirty. Equivalent to ["fail-on-uncommitted-changes", "no-local-version"] as a single scheme name. Recommended for release CI where you want PyPI-compatible versions and an explicit build failure on uncommitted changes.
fail-on-uncommitted-changes

When the working tree is dirty (ScmVersion.dirty is true), raises DirtyWorkingTreeError. When clean, returns None so the next scheme in a local_scheme list runs—pair it with your usual local scheme (for example node-and-date or no-local-version). This works with any version_scheme, so release CI can enforce a clean tree without a separate implementation per version scheme.

Example: local_scheme = ["fail-on-uncommitted-changes", "node-and-date"]

See Configuration Overrides for enabling this via environment variables in CI without editing pyproject.toml.