| """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 warnings
|
| import time
|
| import collections
|
|
|
| from .._importlib import metadata
|
| from .. import _entry_points
|
|
|
| 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
|
| from pkg_resources import (
|
| Requirement, safe_name, parse_version,
|
| safe_version, to_filename)
|
| 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 setuptools import SetuptoolsDeprecationWarning
|
|
|
|
|
| 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 safe_name(self.distribution.get_name())
|
|
|
| def tagged_version(self):
|
| return safe_version(self._maybe_tag(self.distribution.get_version()))
|
|
|
| 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 safe_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.broken_egg_info = False
|
| 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 = parse_version(self.egg_version)
|
|
|
| try:
|
| is_version = isinstance(parsed_version, packaging.version.Version)
|
| spec = "%s==%s" if is_version else "%s===%s"
|
| 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 = to_filename(self.egg_name) + '.egg-info'
|
| if self.egg_base != os.curdir:
|
| self.egg_info = os.path.join(self.egg_base, self.egg_info)
|
| if '-' in self.egg_name:
|
| self.check_broken_egg_info()
|
|
|
|
|
|
|
|
|
| self.distribution.metadata.version = self.egg_version
|
|
|
|
|
|
|
|
|
|
|
| pd = self.distribution._patched_dist
|
| if pd is not None and pd.key == self.egg_name.lower():
|
| pd._version = self.egg_version
|
| pd._parsed_version = parse_version(self.egg_version)
|
| self.distribution._patched_dist = None
|
|
|
| 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)
|
| os.utime(self.egg_info, None)
|
| 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
|
|
|
| def check_broken_egg_info(self):
|
| bei = self.egg_name + '.egg-info'
|
| if self.egg_base != os.curdir:
|
| bei = os.path.join(self.egg_base, bei)
|
| if os.path.exists(bei):
|
| log.warn(
|
| "-" * 78 + '\n'
|
| "Note: Your current .egg-info directory has a '-' in its name;"
|
| '\nthis will not work correctly with "setup.py develop".\n\n'
|
| 'Please rename %s to %s to correct this problem.\n' + '-' * 78,
|
| bei, self.egg_info
|
| )
|
| self.broken_egg_info = self.egg_info
|
| self.egg_info = bei
|
|
|
|
|
| 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.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)
|
| pass
|
| self.filelist.extend(license_files)
|
|
|
| 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()
|
|
|
| warnings.warn(
|
| "Custom 'build_py' does not implement "
|
| "'get_data_files_without_manifest'.\nPlease extend command classes"
|
| " from setuptools instead of distutils.",
|
| SetuptoolsDeprecationWarning
|
| )
|
| 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):
|
| if os.path.exists(filename):
|
| log.warn(
|
| "WARNING: 'depends.txt' is not used by setuptools 0.6!\n"
|
| "Use the install_requires/extras_require setup() args instead."
|
| )
|
|
|
|
|
| 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 get_pkg_info_revision():
|
| """
|
| Get a -r### off of PKG-INFO Version in case this is an sdist of
|
| a subversion revision.
|
| """
|
| warnings.warn(
|
| "get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning)
|
| if os.path.exists('PKG-INFO'):
|
| with io.open('PKG-INFO') as f:
|
| for line in f:
|
| match = re.match(r"Version:.*-r(\d+)\s*$", line)
|
| if match:
|
| return int(match.group(1))
|
| return 0
|
|
|
|
|
| class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):
|
| """Deprecated behavior warning for EggInfo, bypassing suppression."""
|
|
|