Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
it is strongly recommended to use distribution-specific pretend versions
(see below).

`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}`
`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}`
: used as the primary source for the version number,
in which case it will be an unparsed string.
Specifying distribution-specific pretend versions will
Expand Down
8 changes: 4 additions & 4 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ build:
- export SETUPTOOLS_SCM_OVERRIDES_FOR_${READTHEDOCS_PROJECT//-/_}='{scm.git.pre_parse="fail_on_shallow"}'
```

This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow.
This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow.

## CI/CD and Package Publishing

Expand All @@ -67,7 +67,7 @@ These local version components (`+g1a2b3c4d5`, `+dirty`) prevent uploading to Py

#### The Solution

Use the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `local_scheme` to `no-local-version` when building for upload to PyPI.
Use the `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` environment variable to override the `local_scheme` to `no-local-version` when building for upload to PyPI.

### GitHub Actions Example

Expand Down Expand Up @@ -287,9 +287,9 @@ publish-release:

#### Environment Variable Format

The environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` must be set where:
The environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` must be set where:

1. **`${NORMALIZED_DIST_NAME}`** is your package name normalized according to PEP 503:
1. **`${DIST_NAME}`** is your package name normalized according to PEP 503:
- Convert to uppercase
- Replace hyphens and dots with underscores
- Examples: `my-package` → `MY_PACKAGE`, `my.package` → `MY_PACKAGE`
Expand Down
6 changes: 3 additions & 3 deletions docs/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ setuptools-scm provides a mechanism to override the version number build time.
the environment variable `SETUPTOOLS_SCM_PRETEND_VERSION` is used
as the override source for the version number unparsed string.

to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}`
to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}`
where the dist name normalization follows adapted PEP 503 semantics.

## pretend metadata
Expand All @@ -17,7 +17,7 @@ setuptools-scm provides a mechanism to override individual version metadata fiel
The environment variable `SETUPTOOLS_SCM_PRETEND_METADATA` accepts a TOML inline table
with field overrides for the ScmVersion object.

To be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${NORMALIZED_DIST_NAME}`
To be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${DIST_NAME}`
where the dist name normalization follows adapted PEP 503 semantics.

### Supported fields
Expand Down Expand Up @@ -82,7 +82,7 @@ export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}'

## config overrides

setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}`
setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}`
as a toml inline map to override the configuration data from `pyproject.toml`.

## subprocess timeouts
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ Note that running this Dockerfile requires docker with BuildKit enabled

To avoid BuildKit and mounting of the .git folder altogether, one can also pass the desired
version as a build argument.
Note that `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}`
Note that `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}`
is preferred over `SETUPTOOLS_SCM_PRETEND_VERSION`.


Expand Down
140 changes: 132 additions & 8 deletions src/setuptools_scm/_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import dataclasses
import os
import re

from difflib import get_close_matches
from typing import Any
from typing import Mapping

from packaging.utils import canonicalize_name

from . import _config
from . import _log
Expand All @@ -19,18 +22,139 @@
PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}"


def _search_env_vars_with_prefix(
prefix: str, dist_name: str, env: Mapping[str, str]
) -> list[tuple[str, str]]:
"""Search environment variables with a given prefix for potential dist name matches.

Args:
prefix: The environment variable prefix (e.g., "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_")
dist_name: The original dist name to match against
env: Environment dictionary to search in

Returns:
List of (env_var_name, env_var_value) tuples for potential matches
"""
# Get the canonical name for comparison
canonical_dist_name = canonicalize_name(dist_name)

matches = []
for env_var, value in env.items():
if env_var.startswith(prefix):
suffix = env_var[len(prefix) :]
# Normalize the suffix and compare to canonical dist name
try:
normalized_suffix = canonicalize_name(suffix.lower().replace("_", "-"))
if normalized_suffix == canonical_dist_name:
matches.append((env_var, value))
except Exception:
# If normalization fails for any reason, skip this env var
continue

return matches


def _find_close_env_var_matches(
prefix: str, expected_suffix: str, env: Mapping[str, str], threshold: float = 0.6
) -> list[str]:
"""Find environment variables with similar suffixes that might be typos.

Args:
prefix: The environment variable prefix
expected_suffix: The expected suffix (canonicalized dist name in env var format)
env: Environment dictionary to search in
threshold: Similarity threshold for matches (0.0 to 1.0)

Returns:
List of environment variable names that are close matches
"""
candidates = []
for env_var in env:
if env_var.startswith(prefix):
suffix = env_var[len(prefix) :]
candidates.append(suffix)

# Use difflib to find close matches
close_matches = get_close_matches(
expected_suffix, candidates, n=3, cutoff=threshold
)

return [f"{prefix}{match}" for match in close_matches if match != expected_suffix]


def read_named_env(
*, tool: str = "SETUPTOOLS_SCM", name: str, dist_name: str | None
*,
tool: str = "SETUPTOOLS_SCM",
name: str,
dist_name: str | None,
env: Mapping[str, str] = os.environ,
) -> str | None:
""" """
"""Read a named environment variable, with fallback search for dist-specific variants.

This function first tries the standard normalized environment variable name.
If that's not found and a dist_name is provided, it searches for alternative
normalizations and warns about potential issues.

Args:
tool: The tool prefix (default: "SETUPTOOLS_SCM")
name: The environment variable name component
dist_name: The distribution name for dist-specific variables
env: Environment dictionary to search in (defaults to os.environ)

Returns:
The environment variable value if found, None otherwise
"""

# First try the generic version
generic_val = env.get(f"{tool}_{name}")

if dist_name is not None:
# Normalize the dist name as per PEP 503.
normalized_dist_name = re.sub(r"[-_.]+", "-", dist_name)
env_var_dist_name = normalized_dist_name.replace("-", "_").upper()
val = os.environ.get(f"{tool}_{name}_FOR_{env_var_dist_name}")
# Normalize the dist name using packaging.utils.canonicalize_name
canonical_dist_name = canonicalize_name(dist_name)
env_var_dist_name = canonical_dist_name.replace("-", "_").upper()
expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}"

# Try the standard normalized name first
val = env.get(expected_env_var)
if val is not None:
return val
return os.environ.get(f"{tool}_{name}")

# If not found, search for alternative normalizations
prefix = f"{tool}_{name}_FOR_"
alternative_matches = _search_env_vars_with_prefix(prefix, dist_name, env)

if alternative_matches:
# Found alternative matches - use the first one but warn
env_var, value = alternative_matches[0]
log.warning(
"Found environment variable '%s' for dist name '%s', "
"but expected '%s'. Consider using the standard normalized name.",
env_var,
dist_name,
expected_env_var,
)
if len(alternative_matches) > 1:
other_vars = [var for var, _ in alternative_matches[1:]]
log.warning(
"Multiple alternative environment variables found: %s. Using '%s'.",
other_vars,
env_var,
)
return value

# No exact or alternative matches found - look for potential typos
close_matches = _find_close_env_var_matches(prefix, env_var_dist_name, env)
if close_matches:
log.warning(
"Environment variable '%s' not found for dist name '%s' "
"(canonicalized as '%s'). Did you mean one of these? %s",
expected_env_var,
dist_name,
canonical_dist_name,
close_matches,
)

return generic_val


def _read_pretended_metadata_for(
Expand Down
Loading
Loading