Skip to content

Commit becaeb4

Browse files
hardend environment override finding
- match non-normalized suffixes - show close matches on miss for typos
1 parent 9aa1d75 commit becaeb4

File tree

2 files changed

+378
-8
lines changed

2 files changed

+378
-8
lines changed

src/setuptools_scm/_overrides.py

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import dataclasses
44
import os
5-
import re
65

6+
from difflib import get_close_matches
77
from typing import Any
8+
from typing import Mapping
9+
10+
from packaging.utils import canonicalize_name
811

912
from . import _config
1013
from . import _log
@@ -19,18 +22,139 @@
1922
PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}"
2023

2124

25+
def _search_env_vars_with_prefix(
26+
prefix: str, dist_name: str, env: Mapping[str, str]
27+
) -> list[tuple[str, str]]:
28+
"""Search environment variables with a given prefix for potential dist name matches.
29+
30+
Args:
31+
prefix: The environment variable prefix (e.g., "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_")
32+
dist_name: The original dist name to match against
33+
env: Environment dictionary to search in
34+
35+
Returns:
36+
List of (env_var_name, env_var_value) tuples for potential matches
37+
"""
38+
# Get the canonical name for comparison
39+
canonical_dist_name = canonicalize_name(dist_name)
40+
41+
matches = []
42+
for env_var, value in env.items():
43+
if env_var.startswith(prefix):
44+
suffix = env_var[len(prefix) :]
45+
# Normalize the suffix and compare to canonical dist name
46+
try:
47+
normalized_suffix = canonicalize_name(suffix.lower().replace("_", "-"))
48+
if normalized_suffix == canonical_dist_name:
49+
matches.append((env_var, value))
50+
except Exception:
51+
# If normalization fails for any reason, skip this env var
52+
continue
53+
54+
return matches
55+
56+
57+
def _find_close_env_var_matches(
58+
prefix: str, expected_suffix: str, env: Mapping[str, str], threshold: float = 0.6
59+
) -> list[str]:
60+
"""Find environment variables with similar suffixes that might be typos.
61+
62+
Args:
63+
prefix: The environment variable prefix
64+
expected_suffix: The expected suffix (canonicalized dist name in env var format)
65+
env: Environment dictionary to search in
66+
threshold: Similarity threshold for matches (0.0 to 1.0)
67+
68+
Returns:
69+
List of environment variable names that are close matches
70+
"""
71+
candidates = []
72+
for env_var in env:
73+
if env_var.startswith(prefix):
74+
suffix = env_var[len(prefix) :]
75+
candidates.append(suffix)
76+
77+
# Use difflib to find close matches
78+
close_matches = get_close_matches(
79+
expected_suffix, candidates, n=3, cutoff=threshold
80+
)
81+
82+
return [f"{prefix}{match}" for match in close_matches if match != expected_suffix]
83+
84+
2285
def read_named_env(
23-
*, tool: str = "SETUPTOOLS_SCM", name: str, dist_name: str | None
86+
*,
87+
tool: str = "SETUPTOOLS_SCM",
88+
name: str,
89+
dist_name: str | None,
90+
env: Mapping[str, str] = os.environ,
2491
) -> str | None:
25-
""" """
92+
"""Read a named environment variable, with fallback search for dist-specific variants.
93+
94+
This function first tries the standard normalized environment variable name.
95+
If that's not found and a dist_name is provided, it searches for alternative
96+
normalizations and warns about potential issues.
97+
98+
Args:
99+
tool: The tool prefix (default: "SETUPTOOLS_SCM")
100+
name: The environment variable name component
101+
dist_name: The distribution name for dist-specific variables
102+
env: Environment dictionary to search in (defaults to os.environ)
103+
104+
Returns:
105+
The environment variable value if found, None otherwise
106+
"""
107+
108+
# First try the generic version
109+
generic_val = env.get(f"{tool}_{name}")
110+
26111
if dist_name is not None:
27-
# Normalize the dist name as per PEP 503.
28-
normalized_dist_name = re.sub(r"[-_.]+", "-", dist_name)
29-
env_var_dist_name = normalized_dist_name.replace("-", "_").upper()
30-
val = os.environ.get(f"{tool}_{name}_FOR_{env_var_dist_name}")
112+
# Normalize the dist name using packaging.utils.canonicalize_name
113+
canonical_dist_name = canonicalize_name(dist_name)
114+
env_var_dist_name = canonical_dist_name.replace("-", "_").upper()
115+
expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}"
116+
117+
# Try the standard normalized name first
118+
val = env.get(expected_env_var)
31119
if val is not None:
32120
return val
33-
return os.environ.get(f"{tool}_{name}")
121+
122+
# If not found, search for alternative normalizations
123+
prefix = f"{tool}_{name}_FOR_"
124+
alternative_matches = _search_env_vars_with_prefix(prefix, dist_name, env)
125+
126+
if alternative_matches:
127+
# Found alternative matches - use the first one but warn
128+
env_var, value = alternative_matches[0]
129+
log.warning(
130+
"Found environment variable '%s' for dist name '%s', "
131+
"but expected '%s'. Consider using the standard normalized name.",
132+
env_var,
133+
dist_name,
134+
expected_env_var,
135+
)
136+
if len(alternative_matches) > 1:
137+
other_vars = [var for var, _ in alternative_matches[1:]]
138+
log.warning(
139+
"Multiple alternative environment variables found: %s. Using '%s'.",
140+
other_vars,
141+
env_var,
142+
)
143+
return value
144+
145+
# No exact or alternative matches found - look for potential typos
146+
close_matches = _find_close_env_var_matches(prefix, env_var_dist_name, env)
147+
if close_matches:
148+
log.warning(
149+
"Environment variable '%s' not found for dist name '%s' "
150+
"(canonicalized as '%s'). Did you mean one of these? %s",
151+
expected_env_var,
152+
dist_name,
153+
canonical_dist_name,
154+
close_matches,
155+
)
156+
157+
return generic_val
34158

35159

36160
def _read_pretended_metadata_for(

0 commit comments

Comments
 (0)