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 setuptools_scm.version.ScmVersion instance created by the function setuptools_scm.version.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 then name of your SCM control directory as name of the entrypoint.

api reference for scm version objects

setuptools_scm.version.ScmVersion dataclass

represents a parsed version from scm

Source code in src/setuptools_scm/version.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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
@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)
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 src/setuptools_scm/version.py
206
207
208
209
210
211
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 src/setuptools_scm/version.py
193
194
195
196
197
198
199
200
201
202
203
204
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,
    )

setuptools_scm.version.meta

meta(tag: str | _VersionT, *, 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 src/setuptools_scm/version.py
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
def meta(
    tag: str | _VersionT,
    *,
    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: _VersionT
    # 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}"
    scm_version = ScmVersion(
        parsed_version,
        distance=distance,
        node=node,
        dirty=dirty,
        preformatted=preformatted,
        branch=branch,
        config=config,
        node_date=node_date,
    )
    if time is not None:
        scm_version = dataclasses.replace(scm_version, time=time)
    return scm_version

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)

python-simplified-semver

Basic semantic versioning.

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.

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

release-branch-semver

Semantic versioning 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.

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 and should return a string representing the local version. 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