1"""
2Helpers for normalization as expected in wheel/sdist/module file names
3and core metadata
4"""
5
6import re
7
8import packaging
9
10# https://packaging.python.org/en/latest/specifications/core-metadata/#name
11_VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I)
12_UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9._-]+", re.I)
13_NON_ALPHANUMERIC = re.compile(r"[^A-Z0-9]+", re.I)
14_PEP440_FALLBACK = re.compile(r"^v?(?P<safe>(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I)
15
16
17def safe_identifier(name: str) -> str:
18 """Make a string safe to be used as Python identifier.
19 >>> safe_identifier("12abc")
20 '_12abc'
21 >>> safe_identifier("__editable__.myns.pkg-78.9.3_local")
22 '__editable___myns_pkg_78_9_3_local'
23 """
24 safe = re.sub(r'\W|^(?=\d)', '_', name)
25 assert safe.isidentifier()
26 return safe
27
28
29def safe_name(component: str) -> str:
30 """Escape a component used as a project name according to Core Metadata.
31 >>> safe_name("hello world")
32 'hello-world'
33 >>> safe_name("hello?world")
34 'hello-world'
35 >>> safe_name("hello_world")
36 'hello_world'
37 """
38 # See pkg_resources.safe_name
39 return _UNSAFE_NAME_CHARS.sub("-", component)
40
41
42def safe_version(version: str) -> str:
43 """Convert an arbitrary string into a valid version string.
44 Can still raise an ``InvalidVersion`` exception.
45 To avoid exceptions use ``best_effort_version``.
46 >>> safe_version("1988 12 25")
47 '1988.12.25'
48 >>> safe_version("v0.2.1")
49 '0.2.1'
50 >>> safe_version("v0.2?beta")
51 '0.2b0'
52 >>> safe_version("v0.2 beta")
53 '0.2b0'
54 >>> safe_version("ubuntu lts")
55 Traceback (most recent call last):
56 ...
57 packaging.version.InvalidVersion: Invalid version: 'ubuntu.lts'
58 """
59 v = version.replace(' ', '.')
60 try:
61 return str(packaging.version.Version(v))
62 except packaging.version.InvalidVersion:
63 attempt = _UNSAFE_NAME_CHARS.sub("-", v)
64 return str(packaging.version.Version(attempt))
65
66
67def best_effort_version(version: str) -> str:
68 """Convert an arbitrary string into a version-like string.
69 Fallback when ``safe_version`` is not safe enough.
70 >>> best_effort_version("v0.2 beta")
71 '0.2b0'
72 >>> best_effort_version("ubuntu lts")
73 '0.dev0+sanitized.ubuntu.lts'
74 >>> best_effort_version("0.23ubuntu1")
75 '0.23.dev0+sanitized.ubuntu1'
76 >>> best_effort_version("0.23-")
77 '0.23.dev0+sanitized'
78 >>> best_effort_version("0.-_")
79 '0.dev0+sanitized'
80 >>> best_effort_version("42.+?1")
81 '42.dev0+sanitized.1'
82 """
83 # See pkg_resources._forgiving_version
84 try:
85 return safe_version(version)
86 except packaging.version.InvalidVersion:
87 v = version.replace(' ', '.')
88 match = _PEP440_FALLBACK.search(v)
89 if match:
90 safe = match["safe"]
91 rest = v[len(safe) :]
92 else:
93 safe = "0"
94 rest = version
95 safe_rest = _NON_ALPHANUMERIC.sub(".", rest).strip(".")
96 local = f"sanitized.{safe_rest}".strip(".")
97 return safe_version(f"{safe}.dev0+{local}")
98
99
100def safe_extra(extra: str) -> str:
101 """Normalize extra name according to PEP 685
102 >>> safe_extra("_FrIeNdLy-._.-bArD")
103 'friendly-bard'
104 >>> safe_extra("FrIeNdLy-._.-bArD__._-")
105 'friendly-bard'
106 """
107 return _NON_ALPHANUMERIC.sub("-", extra).strip("-").lower()
108
109
110def filename_component(value: str) -> str:
111 """Normalize each component of a filename (e.g. distribution/version part of wheel)
112 Note: ``value`` needs to be already normalized.
113 >>> filename_component("my-pkg")
114 'my_pkg'
115 """
116 return value.replace("-", "_").strip("_")
117
118
119def filename_component_broken(value: str) -> str:
120 """
121 Produce the incorrect filename component for compatibility.
122
123 See pypa/setuptools#4167 for detailed analysis.
124
125 TODO: replace this with filename_component after pip 24 is
126 nearly-ubiquitous.
127
128 >>> filename_component_broken('foo_bar-baz')
129 'foo-bar-baz'
130 """
131 return value.replace('_', '-')
132
133
134def safer_name(value: str) -> str:
135 """Like ``safe_name`` but can be used as filename component for wheel"""
136 # See bdist_wheel.safer_name
137 return filename_component(safe_name(value))
138
139
140def safer_best_effort_version(value: str) -> str:
141 """Like ``best_effort_version`` but can be used as filename component for wheel"""
142 # See bdist_wheel.safer_verion
143 # TODO: Replace with only safe_version in the future (no need for best effort)
144 return filename_component(best_effort_version(value))