|
2 | 2 |
|
3 | 3 | import dataclasses
|
4 | 4 | import os
|
5 |
| -import re |
6 | 5 |
|
| 6 | +from difflib import get_close_matches |
7 | 7 | from typing import Any
|
| 8 | +from typing import Mapping |
| 9 | + |
| 10 | +from packaging.utils import canonicalize_name |
8 | 11 |
|
9 | 12 | from . import _config
|
10 | 13 | from . import _log
|
|
19 | 22 | PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}"
|
20 | 23 |
|
21 | 24 |
|
| 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 | + |
22 | 85 | 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, |
24 | 91 | ) -> 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 | + |
26 | 111 | 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) |
31 | 119 | if val is not None:
|
32 | 120 | 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 |
34 | 158 |
|
35 | 159 |
|
36 | 160 | def _read_pretended_metadata_for(
|
|
0 commit comments