|
"""setuptools.command.egg_info |
|
|
|
Create a distribution's .egg-info directory and contents""" |
|
|
|
from distutils.filelist import FileList as _FileList |
|
from distutils.errors import DistutilsInternalError |
|
from distutils.util import convert_path |
|
from distutils import log |
|
import distutils.errors |
|
import distutils.filelist |
|
import functools |
|
import os |
|
import re |
|
import sys |
|
import io |
|
import time |
|
import collections |
|
|
|
from .._importlib import metadata |
|
from .. import _entry_points, _normalization |
|
|
|
from setuptools import Command |
|
from setuptools.command.sdist import sdist |
|
from setuptools.command.sdist import walk_revctrl |
|
from setuptools.command.setopt import edit_config |
|
from setuptools.command import bdist_egg |
|
import setuptools.unicode_utils as unicode_utils |
|
from setuptools.glob import glob |
|
|
|
from setuptools.extern import packaging |
|
from setuptools.extern.jaraco.text import yield_lines |
|
from ..warnings import SetuptoolsDeprecationWarning |
|
|
|
|
|
PY_MAJOR = '{}.{}'.format(*sys.version_info) |
|
|
|
|
|
def translate_pattern(glob): |
|
""" |
|
Translate a file path glob like '*.txt' in to a regular expression. |
|
This differs from fnmatch.translate which allows wildcards to match |
|
directory separators. It also knows about '**/' which matches any number of |
|
directories. |
|
""" |
|
pat = '' |
|
|
|
|
|
chunks = glob.split(os.path.sep) |
|
|
|
sep = re.escape(os.sep) |
|
valid_char = '[^%s]' % (sep,) |
|
|
|
for c, chunk in enumerate(chunks): |
|
last_chunk = c == len(chunks) - 1 |
|
|
|
|
|
if chunk == '**': |
|
if last_chunk: |
|
|
|
pat += '.*' |
|
else: |
|
|
|
pat += '(?:%s+%s)*' % (valid_char, sep) |
|
continue |
|
|
|
|
|
i = 0 |
|
chunk_len = len(chunk) |
|
while i < chunk_len: |
|
char = chunk[i] |
|
if char == '*': |
|
|
|
pat += valid_char + '*' |
|
elif char == '?': |
|
|
|
pat += valid_char |
|
elif char == '[': |
|
|
|
inner_i = i + 1 |
|
|
|
if inner_i < chunk_len and chunk[inner_i] == '!': |
|
inner_i = inner_i + 1 |
|
if inner_i < chunk_len and chunk[inner_i] == ']': |
|
inner_i = inner_i + 1 |
|
|
|
|
|
while inner_i < chunk_len and chunk[inner_i] != ']': |
|
inner_i = inner_i + 1 |
|
|
|
if inner_i >= chunk_len: |
|
|
|
|
|
pat += re.escape(char) |
|
else: |
|
|
|
inner = chunk[i + 1:inner_i] |
|
char_class = '' |
|
|
|
|
|
if inner[0] == '!': |
|
char_class = '^' |
|
inner = inner[1:] |
|
|
|
char_class += re.escape(inner) |
|
pat += '[%s]' % (char_class,) |
|
|
|
|
|
i = inner_i |
|
else: |
|
pat += re.escape(char) |
|
i += 1 |
|
|
|
|
|
if not last_chunk: |
|
pat += sep |
|
|
|
pat += r'\Z' |
|
return re.compile(pat, flags=re.MULTILINE | re.DOTALL) |
|
|
|
|
|
class InfoCommon: |
|
tag_build = None |
|
tag_date = None |
|
|
|
@property |
|
def name(self): |
|
return _normalization.safe_name(self.distribution.get_name()) |
|
|
|
def tagged_version(self): |
|
tagged = self._maybe_tag(self.distribution.get_version()) |
|
return _normalization.best_effort_version(tagged) |
|
|
|
def _maybe_tag(self, version): |
|
""" |
|
egg_info may be called more than once for a distribution, |
|
in which case the version string already contains all tags. |
|
""" |
|
return ( |
|
version if self.vtags and self._already_tagged(version) |
|
else version + self.vtags |
|
) |
|
|
|
def _already_tagged(self, version: str) -> bool: |
|
|
|
|
|
return version.endswith(self.vtags) or version.endswith(self._safe_tags()) |
|
|
|
def _safe_tags(self) -> str: |
|
|
|
|
|
return _normalization.best_effort_version(f"0{self.vtags}")[1:] |
|
|
|
def tags(self) -> str: |
|
version = '' |
|
if self.tag_build: |
|
version += self.tag_build |
|
if self.tag_date: |
|
version += time.strftime("%Y%m%d") |
|
return version |
|
vtags = property(tags) |
|
|
|
|
|
class egg_info(InfoCommon, Command): |
|
description = "create a distribution's .egg-info directory" |
|
|
|
user_options = [ |
|
('egg-base=', 'e', "directory containing .egg-info directories" |
|
" (default: top of the source tree)"), |
|
('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"), |
|
('tag-build=', 'b', "Specify explicit tag to add to version number"), |
|
('no-date', 'D', "Don't include date stamp [default]"), |
|
] |
|
|
|
boolean_options = ['tag-date'] |
|
negative_opt = { |
|
'no-date': 'tag-date', |
|
} |
|
|
|
def initialize_options(self): |
|
self.egg_base = None |
|
self.egg_name = None |
|
self.egg_info = None |
|
self.egg_version = None |
|
self.ignore_egg_info_in_manifest = False |
|
|
|
|
|
|
|
|
|
@property |
|
def tag_svn_revision(self): |
|
pass |
|
|
|
@tag_svn_revision.setter |
|
def tag_svn_revision(self, value): |
|
pass |
|
|
|
|
|
def save_version_info(self, filename): |
|
""" |
|
Materialize the value of date into the |
|
build tag. Install build keys in a deterministic order |
|
to avoid arbitrary reordering on subsequent builds. |
|
""" |
|
egg_info = collections.OrderedDict() |
|
|
|
|
|
egg_info['tag_build'] = self.tags() |
|
egg_info['tag_date'] = 0 |
|
edit_config(filename, dict(egg_info=egg_info)) |
|
|
|
def finalize_options(self): |
|
|
|
|
|
|
|
|
|
self.egg_name = self.name |
|
self.egg_version = self.tagged_version() |
|
parsed_version = packaging.version.Version(self.egg_version) |
|
|
|
try: |
|
is_version = isinstance(parsed_version, packaging.version.Version) |
|
spec = "%s==%s" if is_version else "%s===%s" |
|
packaging.requirements.Requirement(spec % (self.egg_name, self.egg_version)) |
|
except ValueError as e: |
|
raise distutils.errors.DistutilsOptionError( |
|
"Invalid distribution name or version syntax: %s-%s" % |
|
(self.egg_name, self.egg_version) |
|
) from e |
|
|
|
if self.egg_base is None: |
|
dirs = self.distribution.package_dir |
|
self.egg_base = (dirs or {}).get('', os.curdir) |
|
|
|
self.ensure_dirname('egg_base') |
|
self.egg_info = _normalization.filename_component(self.egg_name) + '.egg-info' |
|
if self.egg_base != os.curdir: |
|
self.egg_info = os.path.join(self.egg_base, self.egg_info) |
|
|
|
|
|
|
|
|
|
self.distribution.metadata.version = self.egg_version |
|
|
|
|
|
|
|
|
|
|
|
pd = self.distribution._patched_dist |
|
key = getattr(pd, "key", None) or getattr(pd, "name", None) |
|
if pd is not None and key == self.egg_name.lower(): |
|
pd._version = self.egg_version |
|
pd._parsed_version = packaging.version.Version(self.egg_version) |
|
self.distribution._patched_dist = None |
|
|
|
def _get_egg_basename(self, py_version=PY_MAJOR, platform=None): |
|
"""Compute filename of the output egg. Private API.""" |
|
return _egg_basename(self.egg_name, self.egg_version, py_version, platform) |
|
|
|
def write_or_delete_file(self, what, filename, data, force=False): |
|
"""Write `data` to `filename` or delete if empty |
|
|
|
If `data` is non-empty, this routine is the same as ``write_file()``. |
|
If `data` is empty but not ``None``, this is the same as calling |
|
``delete_file(filename)`. If `data` is ``None``, then this is a no-op |
|
unless `filename` exists, in which case a warning is issued about the |
|
orphaned file (if `force` is false), or deleted (if `force` is true). |
|
""" |
|
if data: |
|
self.write_file(what, filename, data) |
|
elif os.path.exists(filename): |
|
if data is None and not force: |
|
log.warn( |
|
"%s not set in setup(), but %s exists", what, filename |
|
) |
|
return |
|
else: |
|
self.delete_file(filename) |
|
|
|
def write_file(self, what, filename, data): |
|
"""Write `data` to `filename` (if not a dry run) after announcing it |
|
|
|
`what` is used in a log message to identify what is being written |
|
to the file. |
|
""" |
|
log.info("writing %s to %s", what, filename) |
|
data = data.encode("utf-8") |
|
if not self.dry_run: |
|
f = open(filename, 'wb') |
|
f.write(data) |
|
f.close() |
|
|
|
def delete_file(self, filename): |
|
"""Delete `filename` (if not a dry run) after announcing it""" |
|
log.info("deleting %s", filename) |
|
if not self.dry_run: |
|
os.unlink(filename) |
|
|
|
def run(self): |
|
self.mkpath(self.egg_info) |
|
try: |
|
os.utime(self.egg_info, None) |
|
except OSError as e: |
|
msg = f"Cannot update time stamp of directory '{self.egg_info}'" |
|
raise distutils.errors.DistutilsFileError(msg) from e |
|
for ep in metadata.entry_points(group='egg_info.writers'): |
|
writer = ep.load() |
|
writer(self, ep.name, os.path.join(self.egg_info, ep.name)) |
|
|
|
|
|
nl = os.path.join(self.egg_info, "native_libs.txt") |
|
if os.path.exists(nl): |
|
self.delete_file(nl) |
|
|
|
self.find_sources() |
|
|
|
def find_sources(self): |
|
"""Generate SOURCES.txt manifest file""" |
|
manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") |
|
mm = manifest_maker(self.distribution) |
|
mm.ignore_egg_info_dir = self.ignore_egg_info_in_manifest |
|
mm.manifest = manifest_filename |
|
mm.run() |
|
self.filelist = mm.filelist |
|
|
|
|
|
class FileList(_FileList): |
|
|
|
|
|
def __init__(self, warn=None, debug_print=None, ignore_egg_info_dir=False): |
|
super().__init__(warn, debug_print) |
|
self.ignore_egg_info_dir = ignore_egg_info_dir |
|
|
|
def process_template_line(self, line): |
|
|
|
|
|
|
|
|
|
|
|
(action, patterns, dir, dir_pattern) = self._parse_template_line(line) |
|
|
|
action_map = { |
|
'include': self.include, |
|
'exclude': self.exclude, |
|
'global-include': self.global_include, |
|
'global-exclude': self.global_exclude, |
|
'recursive-include': functools.partial( |
|
self.recursive_include, dir, |
|
), |
|
'recursive-exclude': functools.partial( |
|
self.recursive_exclude, dir, |
|
), |
|
'graft': self.graft, |
|
'prune': self.prune, |
|
} |
|
log_map = { |
|
'include': "warning: no files found matching '%s'", |
|
'exclude': ( |
|
"warning: no previously-included files found " |
|
"matching '%s'" |
|
), |
|
'global-include': ( |
|
"warning: no files found matching '%s' " |
|
"anywhere in distribution" |
|
), |
|
'global-exclude': ( |
|
"warning: no previously-included files matching " |
|
"'%s' found anywhere in distribution" |
|
), |
|
'recursive-include': ( |
|
"warning: no files found matching '%s' " |
|
"under directory '%s'" |
|
), |
|
'recursive-exclude': ( |
|
"warning: no previously-included files matching " |
|
"'%s' found under directory '%s'" |
|
), |
|
'graft': "warning: no directories found matching '%s'", |
|
'prune': "no previously-included directories found matching '%s'", |
|
} |
|
|
|
try: |
|
process_action = action_map[action] |
|
except KeyError: |
|
raise DistutilsInternalError( |
|
"this cannot happen: invalid action '{action!s}'". |
|
format(action=action), |
|
) |
|
|
|
|
|
|
|
|
|
|
|
action_is_recursive = action.startswith('recursive-') |
|
if action in {'graft', 'prune'}: |
|
patterns = [dir_pattern] |
|
extra_log_args = (dir, ) if action_is_recursive else () |
|
log_tmpl = log_map[action] |
|
|
|
self.debug_print( |
|
' '.join( |
|
[action] + |
|
([dir] if action_is_recursive else []) + |
|
patterns, |
|
) |
|
) |
|
for pattern in patterns: |
|
if not process_action(pattern): |
|
log.warn(log_tmpl, pattern, *extra_log_args) |
|
|
|
def _remove_files(self, predicate): |
|
""" |
|
Remove all files from the file list that match the predicate. |
|
Return True if any matching files were removed |
|
""" |
|
found = False |
|
for i in range(len(self.files) - 1, -1, -1): |
|
if predicate(self.files[i]): |
|
self.debug_print(" removing " + self.files[i]) |
|
del self.files[i] |
|
found = True |
|
return found |
|
|
|
def include(self, pattern): |
|
"""Include files that match 'pattern'.""" |
|
found = [f for f in glob(pattern) if not os.path.isdir(f)] |
|
self.extend(found) |
|
return bool(found) |
|
|
|
def exclude(self, pattern): |
|
"""Exclude files that match 'pattern'.""" |
|
match = translate_pattern(pattern) |
|
return self._remove_files(match.match) |
|
|
|
def recursive_include(self, dir, pattern): |
|
""" |
|
Include all files anywhere in 'dir/' that match the pattern. |
|
""" |
|
full_pattern = os.path.join(dir, '**', pattern) |
|
found = [f for f in glob(full_pattern, recursive=True) |
|
if not os.path.isdir(f)] |
|
self.extend(found) |
|
return bool(found) |
|
|
|
def recursive_exclude(self, dir, pattern): |
|
""" |
|
Exclude any file anywhere in 'dir/' that match the pattern. |
|
""" |
|
match = translate_pattern(os.path.join(dir, '**', pattern)) |
|
return self._remove_files(match.match) |
|
|
|
def graft(self, dir): |
|
"""Include all files from 'dir/'.""" |
|
found = [ |
|
item |
|
for match_dir in glob(dir) |
|
for item in distutils.filelist.findall(match_dir) |
|
] |
|
self.extend(found) |
|
return bool(found) |
|
|
|
def prune(self, dir): |
|
"""Filter out files from 'dir/'.""" |
|
match = translate_pattern(os.path.join(dir, '**')) |
|
return self._remove_files(match.match) |
|
|
|
def global_include(self, pattern): |
|
""" |
|
Include all files anywhere in the current directory that match the |
|
pattern. This is very inefficient on large file trees. |
|
""" |
|
if self.allfiles is None: |
|
self.findall() |
|
match = translate_pattern(os.path.join('**', pattern)) |
|
found = [f for f in self.allfiles if match.match(f)] |
|
self.extend(found) |
|
return bool(found) |
|
|
|
def global_exclude(self, pattern): |
|
""" |
|
Exclude all files anywhere that match the pattern. |
|
""" |
|
match = translate_pattern(os.path.join('**', pattern)) |
|
return self._remove_files(match.match) |
|
|
|
def append(self, item): |
|
if item.endswith('\r'): |
|
item = item[:-1] |
|
path = convert_path(item) |
|
|
|
if self._safe_path(path): |
|
self.files.append(path) |
|
|
|
def extend(self, paths): |
|
self.files.extend(filter(self._safe_path, paths)) |
|
|
|
def _repair(self): |
|
""" |
|
Replace self.files with only safe paths |
|
|
|
Because some owners of FileList manipulate the underlying |
|
``files`` attribute directly, this method must be called to |
|
repair those paths. |
|
""" |
|
self.files = list(filter(self._safe_path, self.files)) |
|
|
|
def _safe_path(self, path): |
|
enc_warn = "'%s' not %s encodable -- skipping" |
|
|
|
|
|
u_path = unicode_utils.filesys_decode(path) |
|
if u_path is None: |
|
log.warn("'%s' in unexpected encoding -- skipping" % path) |
|
return False |
|
|
|
|
|
utf8_path = unicode_utils.try_encode(u_path, "utf-8") |
|
if utf8_path is None: |
|
log.warn(enc_warn, path, 'utf-8') |
|
return False |
|
|
|
try: |
|
|
|
is_egg_info = ".egg-info" in u_path or b".egg-info" in utf8_path |
|
if self.ignore_egg_info_dir and is_egg_info: |
|
return False |
|
|
|
if os.path.exists(u_path) or os.path.exists(utf8_path): |
|
return True |
|
|
|
except UnicodeEncodeError: |
|
log.warn(enc_warn, path, sys.getfilesystemencoding()) |
|
|
|
|
|
class manifest_maker(sdist): |
|
template = "MANIFEST.in" |
|
|
|
def initialize_options(self): |
|
self.use_defaults = 1 |
|
self.prune = 1 |
|
self.manifest_only = 1 |
|
self.force_manifest = 1 |
|
self.ignore_egg_info_dir = False |
|
|
|
def finalize_options(self): |
|
pass |
|
|
|
def run(self): |
|
self.filelist = FileList(ignore_egg_info_dir=self.ignore_egg_info_dir) |
|
if not os.path.exists(self.manifest): |
|
self.write_manifest() |
|
self.add_defaults() |
|
if os.path.exists(self.template): |
|
self.read_template() |
|
self.add_license_files() |
|
self._add_referenced_files() |
|
self.prune_file_list() |
|
self.filelist.sort() |
|
self.filelist.remove_duplicates() |
|
self.write_manifest() |
|
|
|
def _manifest_normalize(self, path): |
|
path = unicode_utils.filesys_decode(path) |
|
return path.replace(os.sep, '/') |
|
|
|
def write_manifest(self): |
|
""" |
|
Write the file list in 'self.filelist' to the manifest file |
|
named by 'self.manifest'. |
|
""" |
|
self.filelist._repair() |
|
|
|
|
|
files = [self._manifest_normalize(f) for f in self.filelist.files] |
|
msg = "writing manifest file '%s'" % self.manifest |
|
self.execute(write_file, (self.manifest, files), msg) |
|
|
|
def warn(self, msg): |
|
if not self._should_suppress_warning(msg): |
|
sdist.warn(self, msg) |
|
|
|
@staticmethod |
|
def _should_suppress_warning(msg): |
|
""" |
|
suppress missing-file warnings from sdist |
|
""" |
|
return re.match(r"standard file .*not found", msg) |
|
|
|
def add_defaults(self): |
|
sdist.add_defaults(self) |
|
self.filelist.append(self.template) |
|
self.filelist.append(self.manifest) |
|
rcfiles = list(walk_revctrl()) |
|
if rcfiles: |
|
self.filelist.extend(rcfiles) |
|
elif os.path.exists(self.manifest): |
|
self.read_manifest() |
|
|
|
if os.path.exists("setup.py"): |
|
|
|
|
|
self.filelist.append("setup.py") |
|
|
|
ei_cmd = self.get_finalized_command('egg_info') |
|
self.filelist.graft(ei_cmd.egg_info) |
|
|
|
def add_license_files(self): |
|
license_files = self.distribution.metadata.license_files or [] |
|
for lf in license_files: |
|
log.info("adding license file '%s'", lf) |
|
self.filelist.extend(license_files) |
|
|
|
def _add_referenced_files(self): |
|
"""Add files referenced by the config (e.g. `file:` directive) to filelist""" |
|
referenced = getattr(self.distribution, '_referenced_files', []) |
|
|
|
for rf in referenced: |
|
log.debug("adding file referenced by config '%s'", rf) |
|
self.filelist.extend(referenced) |
|
|
|
def prune_file_list(self): |
|
build = self.get_finalized_command('build') |
|
base_dir = self.distribution.get_fullname() |
|
self.filelist.prune(build.build_base) |
|
self.filelist.prune(base_dir) |
|
sep = re.escape(os.sep) |
|
self.filelist.exclude_pattern(r'(^|' + sep + r')(RCS|CVS|\.svn)' + sep, |
|
is_regex=1) |
|
|
|
def _safe_data_files(self, build_py): |
|
""" |
|
The parent class implementation of this method |
|
(``sdist``) will try to include data files, which |
|
might cause recursion problems when |
|
``include_package_data=True``. |
|
|
|
Therefore, avoid triggering any attempt of |
|
analyzing/building the manifest again. |
|
""" |
|
if hasattr(build_py, 'get_data_files_without_manifest'): |
|
return build_py.get_data_files_without_manifest() |
|
|
|
SetuptoolsDeprecationWarning.emit( |
|
"`build_py` command does not inherit from setuptools' `build_py`.", |
|
""" |
|
Custom 'build_py' does not implement 'get_data_files_without_manifest'. |
|
Please extend command classes from setuptools instead of distutils. |
|
""", |
|
see_url="https://peps.python.org/pep-0632/", |
|
|
|
) |
|
return build_py.get_data_files() |
|
|
|
|
|
def write_file(filename, contents): |
|
"""Create a file with the specified name and write 'contents' (a |
|
sequence of strings without line terminators) to it. |
|
""" |
|
contents = "\n".join(contents) |
|
|
|
|
|
contents = contents.encode("utf-8") |
|
|
|
with open(filename, "wb") as f: |
|
f.write(contents) |
|
|
|
|
|
def write_pkg_info(cmd, basename, filename): |
|
log.info("writing %s", filename) |
|
if not cmd.dry_run: |
|
metadata = cmd.distribution.metadata |
|
metadata.version, oldver = cmd.egg_version, metadata.version |
|
metadata.name, oldname = cmd.egg_name, metadata.name |
|
|
|
try: |
|
|
|
|
|
metadata.write_pkg_info(cmd.egg_info) |
|
finally: |
|
metadata.name, metadata.version = oldname, oldver |
|
|
|
safe = getattr(cmd.distribution, 'zip_safe', None) |
|
|
|
bdist_egg.write_safety_flag(cmd.egg_info, safe) |
|
|
|
|
|
def warn_depends_obsolete(cmd, basename, filename): |
|
""" |
|
Unused: left to avoid errors when updating (from source) from <= 67.8. |
|
Old installations have a .dist-info directory with the entry-point |
|
``depends.txt = setuptools.command.egg_info:warn_depends_obsolete``. |
|
This may trigger errors when running the first egg_info in build_meta. |
|
TODO: Remove this function in a version sufficiently > 68. |
|
""" |
|
|
|
|
|
def _write_requirements(stream, reqs): |
|
lines = yield_lines(reqs or ()) |
|
|
|
def append_cr(line): |
|
return line + '\n' |
|
lines = map(append_cr, lines) |
|
stream.writelines(lines) |
|
|
|
|
|
def write_requirements(cmd, basename, filename): |
|
dist = cmd.distribution |
|
data = io.StringIO() |
|
_write_requirements(data, dist.install_requires) |
|
extras_require = dist.extras_require or {} |
|
for extra in sorted(extras_require): |
|
data.write('\n[{extra}]\n'.format(**vars())) |
|
_write_requirements(data, extras_require[extra]) |
|
cmd.write_or_delete_file("requirements", filename, data.getvalue()) |
|
|
|
|
|
def write_setup_requirements(cmd, basename, filename): |
|
data = io.StringIO() |
|
_write_requirements(data, cmd.distribution.setup_requires) |
|
cmd.write_or_delete_file("setup-requirements", filename, data.getvalue()) |
|
|
|
|
|
def write_toplevel_names(cmd, basename, filename): |
|
pkgs = dict.fromkeys( |
|
[ |
|
k.split('.', 1)[0] |
|
for k in cmd.distribution.iter_distribution_names() |
|
] |
|
) |
|
cmd.write_file("top-level names", filename, '\n'.join(sorted(pkgs)) + '\n') |
|
|
|
|
|
def overwrite_arg(cmd, basename, filename): |
|
write_arg(cmd, basename, filename, True) |
|
|
|
|
|
def write_arg(cmd, basename, filename, force=False): |
|
argname = os.path.splitext(basename)[0] |
|
value = getattr(cmd.distribution, argname, None) |
|
if value is not None: |
|
value = '\n'.join(value) + '\n' |
|
cmd.write_or_delete_file(argname, filename, value, force) |
|
|
|
|
|
def write_entries(cmd, basename, filename): |
|
eps = _entry_points.load(cmd.distribution.entry_points) |
|
defn = _entry_points.render(eps) |
|
cmd.write_or_delete_file('entry points', filename, defn, True) |
|
|
|
|
|
def _egg_basename(egg_name, egg_version, py_version=None, platform=None): |
|
"""Compute filename of the output egg. Private API.""" |
|
name = _normalization.filename_component(egg_name) |
|
version = _normalization.filename_component(egg_version) |
|
egg = f"{name}-{version}-py{py_version or PY_MAJOR}" |
|
if platform: |
|
egg += f"-{platform}" |
|
return egg |
|
|
|
|
|
class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning): |
|
"""Deprecated behavior warning for EggInfo, bypassing suppression.""" |
|
|