first commit
This commit is contained in:
286
.venv/Lib/site-packages/setuptools/__init__.py
Normal file
286
.venv/Lib/site-packages/setuptools/__init__.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""Extensions to the 'distutils' for large or complex distributions"""
|
||||
# mypy: disable_error_code=override
|
||||
# Command.reinitialize_command has an extra **kw param that distutils doesn't have
|
||||
# Can't disable on the exact line because distutils doesn't exists on Python 3.12
|
||||
# and mypy isn't aware of distutils_hack, causing distutils.core.Command to be Any,
|
||||
# and a [unused-ignore] to be raised on 3.12+
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, TypeVar, overload
|
||||
|
||||
sys.path.extend(((vendor_path := os.path.join(os.path.dirname(os.path.dirname(__file__)), 'setuptools', '_vendor')) not in sys.path) * [vendor_path]) # fmt: skip
|
||||
# workaround for #4476
|
||||
sys.modules.pop('backports', None)
|
||||
|
||||
import _distutils_hack.override # noqa: F401
|
||||
|
||||
from . import logging, monkey
|
||||
from .depends import Require
|
||||
from .discovery import PackageFinder, PEP420PackageFinder
|
||||
from .dist import Distribution
|
||||
from .extension import Extension
|
||||
from .version import __version__ as __version__
|
||||
from .warnings import SetuptoolsDeprecationWarning
|
||||
|
||||
import distutils.core
|
||||
from distutils.errors import DistutilsOptionError
|
||||
|
||||
__all__ = [
|
||||
'setup',
|
||||
'Distribution',
|
||||
'Command',
|
||||
'Extension',
|
||||
'Require',
|
||||
'SetuptoolsDeprecationWarning',
|
||||
'find_packages',
|
||||
'find_namespace_packages',
|
||||
]
|
||||
|
||||
_CommandT = TypeVar("_CommandT", bound="_Command")
|
||||
|
||||
bootstrap_install_from = None
|
||||
|
||||
find_packages = PackageFinder.find
|
||||
find_namespace_packages = PEP420PackageFinder.find
|
||||
|
||||
|
||||
def _install_setup_requires(attrs):
|
||||
# Note: do not use `setuptools.Distribution` directly, as
|
||||
# our PEP 517 backend patch `distutils.core.Distribution`.
|
||||
class MinimalDistribution(distutils.core.Distribution):
|
||||
"""
|
||||
A minimal version of a distribution for supporting the
|
||||
fetch_build_eggs interface.
|
||||
"""
|
||||
|
||||
def __init__(self, attrs: Mapping[str, object]) -> None:
|
||||
_incl = 'dependency_links', 'setup_requires'
|
||||
filtered = {k: attrs[k] for k in set(_incl) & set(attrs)}
|
||||
super().__init__(filtered)
|
||||
# Prevent accidentally triggering discovery with incomplete set of attrs
|
||||
self.set_defaults._disable()
|
||||
|
||||
def _get_project_config_files(self, filenames=None):
|
||||
"""Ignore ``pyproject.toml``, they are not related to setup_requires"""
|
||||
try:
|
||||
cfg, _toml = super()._split_standard_project_metadata(filenames)
|
||||
except Exception:
|
||||
return filenames, ()
|
||||
return cfg, ()
|
||||
|
||||
def finalize_options(self):
|
||||
"""
|
||||
Disable finalize_options to avoid building the working set.
|
||||
Ref #2158.
|
||||
"""
|
||||
|
||||
dist = MinimalDistribution(attrs)
|
||||
|
||||
# Honor setup.cfg's options.
|
||||
dist.parse_config_files(ignore_option_errors=True)
|
||||
if dist.setup_requires:
|
||||
_fetch_build_eggs(dist)
|
||||
|
||||
|
||||
def _fetch_build_eggs(dist: Distribution):
|
||||
try:
|
||||
dist.fetch_build_eggs(dist.setup_requires)
|
||||
except Exception as ex:
|
||||
msg = """
|
||||
It is possible a package already installed in your system
|
||||
contains an version that is invalid according to PEP 440.
|
||||
You can try `pip install --use-pep517` as a workaround for this problem,
|
||||
or rely on a new virtual environment.
|
||||
|
||||
If the problem refers to a package that is not installed yet,
|
||||
please contact that package's maintainers or distributors.
|
||||
"""
|
||||
if "InvalidVersion" in ex.__class__.__name__:
|
||||
if hasattr(ex, "add_note"):
|
||||
ex.add_note(msg) # PEP 678
|
||||
else:
|
||||
dist.announce(f"\n{msg}\n")
|
||||
raise
|
||||
|
||||
|
||||
def setup(**attrs):
|
||||
logging.configure()
|
||||
# Make sure we have any requirements needed to interpret 'attrs'.
|
||||
_install_setup_requires(attrs)
|
||||
return distutils.core.setup(**attrs)
|
||||
|
||||
|
||||
setup.__doc__ = distutils.core.setup.__doc__
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962
|
||||
from distutils.core import Command as _Command
|
||||
else:
|
||||
_Command = monkey.get_unpatched(distutils.core.Command)
|
||||
|
||||
|
||||
class Command(_Command):
|
||||
"""
|
||||
Setuptools internal actions are organized using a *command design pattern*.
|
||||
This means that each action (or group of closely related actions) executed during
|
||||
the build should be implemented as a ``Command`` subclass.
|
||||
|
||||
These commands are abstractions and do not necessarily correspond to a command that
|
||||
can (or should) be executed via a terminal, in a CLI fashion (although historically
|
||||
they would).
|
||||
|
||||
When creating a new command from scratch, custom defined classes **SHOULD** inherit
|
||||
from ``setuptools.Command`` and implement a few mandatory methods.
|
||||
Between these mandatory methods, are listed:
|
||||
:meth:`initialize_options`, :meth:`finalize_options` and :meth:`run`.
|
||||
|
||||
A useful analogy for command classes is to think of them as subroutines with local
|
||||
variables called "options". The options are "declared" in :meth:`initialize_options`
|
||||
and "defined" (given their final values, aka "finalized") in :meth:`finalize_options`,
|
||||
both of which must be defined by every command class. The "body" of the subroutine,
|
||||
(where it does all the work) is the :meth:`run` method.
|
||||
Between :meth:`initialize_options` and :meth:`finalize_options`, ``setuptools`` may set
|
||||
the values for options/attributes based on user's input (or circumstance),
|
||||
which means that the implementation should be careful to not overwrite values in
|
||||
:meth:`finalize_options` unless necessary.
|
||||
|
||||
Please note that other commands (or other parts of setuptools) may also overwrite
|
||||
the values of the command's options/attributes multiple times during the build
|
||||
process.
|
||||
Therefore it is important to consistently implement :meth:`initialize_options` and
|
||||
:meth:`finalize_options`. For example, all derived attributes (or attributes that
|
||||
depend on the value of other attributes) **SHOULD** be recomputed in
|
||||
:meth:`finalize_options`.
|
||||
|
||||
When overwriting existing commands, custom defined classes **MUST** abide by the
|
||||
same APIs implemented by the original class. They also **SHOULD** inherit from the
|
||||
original class.
|
||||
"""
|
||||
|
||||
command_consumes_arguments = False
|
||||
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
|
||||
|
||||
def __init__(self, dist: Distribution, **kw) -> None:
|
||||
"""
|
||||
Construct the command for dist, updating
|
||||
vars(self) with any keyword parameters.
|
||||
"""
|
||||
super().__init__(dist)
|
||||
vars(self).update(kw)
|
||||
|
||||
def _ensure_stringlike(self, option, what, default=None):
|
||||
val = getattr(self, option)
|
||||
if val is None:
|
||||
setattr(self, option, default)
|
||||
return default
|
||||
elif not isinstance(val, str):
|
||||
raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)")
|
||||
return val
|
||||
|
||||
def ensure_string_list(self, option: str) -> None:
|
||||
r"""Ensure that 'option' is a list of strings. If 'option' is
|
||||
currently a string, we split it either on /,\s*/ or /\s+/, so
|
||||
"foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
|
||||
["foo", "bar", "baz"].
|
||||
|
||||
..
|
||||
TODO: This method seems to be similar to the one in ``distutils.cmd``
|
||||
Probably it is just here for backward compatibility with old Python versions?
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
val = getattr(self, option)
|
||||
if val is None:
|
||||
return
|
||||
elif isinstance(val, str):
|
||||
setattr(self, option, re.split(r',\s*|\s+', val))
|
||||
else:
|
||||
if isinstance(val, list):
|
||||
ok = all(isinstance(v, str) for v in val)
|
||||
else:
|
||||
ok = False
|
||||
if not ok:
|
||||
raise DistutilsOptionError(
|
||||
f"'{option}' must be a list of strings (got {val!r})"
|
||||
)
|
||||
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: str, reinit_subcommands: bool = False, **kw
|
||||
) -> _Command: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: _CommandT, reinit_subcommands: bool = False, **kw
|
||||
) -> _CommandT: ...
|
||||
def reinitialize_command(
|
||||
self, command: str | _Command, reinit_subcommands: bool = False, **kw
|
||||
) -> _Command:
|
||||
cmd = _Command.reinitialize_command(self, command, reinit_subcommands)
|
||||
vars(cmd).update(kw)
|
||||
return cmd # pyright: ignore[reportReturnType] # pypa/distutils#307
|
||||
|
||||
@abstractmethod
|
||||
def initialize_options(self) -> None:
|
||||
"""
|
||||
Set or (reset) all options/attributes/caches used by the command
|
||||
to their default values. Note that these values may be overwritten during
|
||||
the build.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def finalize_options(self) -> None:
|
||||
"""
|
||||
Set final values for all options/attributes used by the command.
|
||||
Most of the time, each option/attribute/cache should only be set if it does not
|
||||
have any value yet (e.g. ``if self.attr is None: self.attr = val``).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Execute the actions intended by the command.
|
||||
(Side effects **SHOULD** only take place when :meth:`run` is executed,
|
||||
for example, creating new files or writing to the terminal output).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _find_all_simple(path):
|
||||
"""
|
||||
Find all files under 'path'
|
||||
"""
|
||||
results = (
|
||||
os.path.join(base, file)
|
||||
for base, dirs, files in os.walk(path, followlinks=True)
|
||||
for file in files
|
||||
)
|
||||
return filter(os.path.isfile, results)
|
||||
|
||||
|
||||
def findall(dir=os.curdir):
|
||||
"""
|
||||
Find all files under 'dir' and return the list of full filenames.
|
||||
Unless dir is '.', return full filenames with dir prepended.
|
||||
"""
|
||||
files = _find_all_simple(dir)
|
||||
if dir == os.curdir:
|
||||
make_rel = functools.partial(os.path.relpath, start=dir)
|
||||
files = map(make_rel, files)
|
||||
return list(files)
|
||||
|
||||
|
||||
class sic(str):
|
||||
"""Treat this string as-is (https://en.wikipedia.org/wiki/Sic)"""
|
||||
|
||||
|
||||
# Apply monkey patches
|
||||
monkey.patch_all()
|
||||
337
.venv/Lib/site-packages/setuptools/_core_metadata.py
Normal file
337
.venv/Lib/site-packages/setuptools/_core_metadata.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Handling of Core Metadata for Python packages (including reading and writing).
|
||||
|
||||
See: https://packaging.python.org/en/latest/specifications/core-metadata/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import stat
|
||||
import textwrap
|
||||
from email import message_from_file
|
||||
from email.message import Message
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from packaging.markers import Marker
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.utils import canonicalize_name, canonicalize_version
|
||||
from packaging.version import Version
|
||||
|
||||
from . import _normalization, _reqs
|
||||
from ._static import is_static
|
||||
from .warnings import SetuptoolsDeprecationWarning
|
||||
|
||||
from distutils.util import rfc822_escape
|
||||
|
||||
|
||||
def get_metadata_version(self):
|
||||
mv = getattr(self, 'metadata_version', None)
|
||||
if mv is None:
|
||||
mv = Version('2.4')
|
||||
self.metadata_version = mv
|
||||
return mv
|
||||
|
||||
|
||||
def rfc822_unescape(content: str) -> str:
|
||||
"""Reverse RFC-822 escaping by removing leading whitespaces from content."""
|
||||
lines = content.splitlines()
|
||||
if len(lines) == 1:
|
||||
return lines[0].lstrip()
|
||||
return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:]))))
|
||||
|
||||
|
||||
def _read_field_from_msg(msg: Message, field: str) -> str | None:
|
||||
"""Read Message header field."""
|
||||
value = msg[field]
|
||||
if value == 'UNKNOWN':
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _read_field_unescaped_from_msg(msg: Message, field: str) -> str | None:
|
||||
"""Read Message header field and apply rfc822_unescape."""
|
||||
value = _read_field_from_msg(msg, field)
|
||||
if value is None:
|
||||
return value
|
||||
return rfc822_unescape(value)
|
||||
|
||||
|
||||
def _read_list_from_msg(msg: Message, field: str) -> list[str] | None:
|
||||
"""Read Message header field and return all results as list."""
|
||||
values = msg.get_all(field, None)
|
||||
if values == []:
|
||||
return None
|
||||
return values
|
||||
|
||||
|
||||
def _read_payload_from_msg(msg: Message) -> str | None:
|
||||
value = str(msg.get_payload()).strip()
|
||||
if value == 'UNKNOWN' or not value:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def read_pkg_file(self, file):
|
||||
"""Reads the metadata values from a file object."""
|
||||
msg = message_from_file(file)
|
||||
|
||||
self.metadata_version = Version(msg['metadata-version'])
|
||||
self.name = _read_field_from_msg(msg, 'name')
|
||||
self.version = _read_field_from_msg(msg, 'version')
|
||||
self.description = _read_field_from_msg(msg, 'summary')
|
||||
# we are filling author only.
|
||||
self.author = _read_field_from_msg(msg, 'author')
|
||||
self.maintainer = None
|
||||
self.author_email = _read_field_from_msg(msg, 'author-email')
|
||||
self.maintainer_email = None
|
||||
self.url = _read_field_from_msg(msg, 'home-page')
|
||||
self.download_url = _read_field_from_msg(msg, 'download-url')
|
||||
self.license = _read_field_unescaped_from_msg(msg, 'license')
|
||||
self.license_expression = _read_field_unescaped_from_msg(msg, 'license-expression')
|
||||
|
||||
self.long_description = _read_field_unescaped_from_msg(msg, 'description')
|
||||
if self.long_description is None and self.metadata_version >= Version('2.1'):
|
||||
self.long_description = _read_payload_from_msg(msg)
|
||||
self.description = _read_field_from_msg(msg, 'summary')
|
||||
|
||||
if 'keywords' in msg:
|
||||
self.keywords = _read_field_from_msg(msg, 'keywords').split(',')
|
||||
|
||||
self.platforms = _read_list_from_msg(msg, 'platform')
|
||||
self.classifiers = _read_list_from_msg(msg, 'classifier')
|
||||
|
||||
# PEP 314 - these fields only exist in 1.1
|
||||
if self.metadata_version == Version('1.1'):
|
||||
self.requires = _read_list_from_msg(msg, 'requires')
|
||||
self.provides = _read_list_from_msg(msg, 'provides')
|
||||
self.obsoletes = _read_list_from_msg(msg, 'obsoletes')
|
||||
else:
|
||||
self.requires = None
|
||||
self.provides = None
|
||||
self.obsoletes = None
|
||||
|
||||
self.license_files = _read_list_from_msg(msg, 'license-file')
|
||||
|
||||
|
||||
def single_line(val):
|
||||
"""
|
||||
Quick and dirty validation for Summary pypa/setuptools#1390.
|
||||
"""
|
||||
if '\n' in val:
|
||||
# TODO: Replace with `raise ValueError("newlines not allowed")`
|
||||
# after reviewing #2893.
|
||||
msg = "newlines are not allowed in `summary` and will break in the future"
|
||||
SetuptoolsDeprecationWarning.emit("Invalid config.", msg)
|
||||
# due_date is undefined. Controversial change, there was a lot of push back.
|
||||
val = val.strip().split('\n')[0]
|
||||
return val
|
||||
|
||||
|
||||
def write_pkg_info(self, base_dir):
|
||||
"""Write the PKG-INFO file into the release tree."""
|
||||
temp = ""
|
||||
final = os.path.join(base_dir, 'PKG-INFO')
|
||||
try:
|
||||
# Use a temporary file while writing to avoid race conditions
|
||||
# (e.g. `importlib.metadata` reading `.egg-info/PKG-INFO`):
|
||||
with NamedTemporaryFile("w", encoding="utf-8", dir=base_dir, delete=False) as f:
|
||||
temp = f.name
|
||||
self.write_pkg_file(f)
|
||||
permissions = stat.S_IMODE(os.lstat(temp).st_mode)
|
||||
os.chmod(temp, permissions | stat.S_IRGRP | stat.S_IROTH)
|
||||
os.replace(temp, final) # atomic operation.
|
||||
finally:
|
||||
if temp and os.path.exists(temp):
|
||||
os.remove(temp)
|
||||
|
||||
|
||||
# Based on Python 3.5 version
|
||||
def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
|
||||
"""Write the PKG-INFO format data to a file object."""
|
||||
version = self.get_metadata_version()
|
||||
|
||||
def write_field(key, value):
|
||||
file.write(f"{key}: {value}\n")
|
||||
|
||||
write_field('Metadata-Version', str(version))
|
||||
write_field('Name', self.get_name())
|
||||
write_field('Version', self.get_version())
|
||||
|
||||
summary = self.get_description()
|
||||
if summary:
|
||||
write_field('Summary', single_line(summary))
|
||||
|
||||
optional_fields = (
|
||||
('Home-page', 'url'),
|
||||
('Download-URL', 'download_url'),
|
||||
('Author', 'author'),
|
||||
('Author-email', 'author_email'),
|
||||
('Maintainer', 'maintainer'),
|
||||
('Maintainer-email', 'maintainer_email'),
|
||||
)
|
||||
|
||||
for field, attr in optional_fields:
|
||||
attr_val = getattr(self, attr, None)
|
||||
if attr_val is not None:
|
||||
write_field(field, attr_val)
|
||||
|
||||
if license_expression := self.license_expression:
|
||||
write_field('License-Expression', license_expression)
|
||||
elif license := self.get_license():
|
||||
write_field('License', rfc822_escape(license))
|
||||
|
||||
for label, url in self.project_urls.items():
|
||||
write_field('Project-URL', f'{label}, {url}')
|
||||
|
||||
keywords = ','.join(self.get_keywords())
|
||||
if keywords:
|
||||
write_field('Keywords', keywords)
|
||||
|
||||
platforms = self.get_platforms() or []
|
||||
for platform in platforms:
|
||||
write_field('Platform', platform)
|
||||
|
||||
self._write_list(file, 'Classifier', self.get_classifiers())
|
||||
|
||||
# PEP 314
|
||||
self._write_list(file, 'Requires', self.get_requires())
|
||||
self._write_list(file, 'Provides', self.get_provides())
|
||||
self._write_list(file, 'Obsoletes', self.get_obsoletes())
|
||||
|
||||
# Setuptools specific for PEP 345
|
||||
if hasattr(self, 'python_requires'):
|
||||
write_field('Requires-Python', self.python_requires)
|
||||
|
||||
# PEP 566
|
||||
if self.long_description_content_type:
|
||||
write_field('Description-Content-Type', self.long_description_content_type)
|
||||
|
||||
safe_license_files = map(_safe_license_file, self.license_files or [])
|
||||
self._write_list(file, 'License-File', safe_license_files)
|
||||
_write_requirements(self, file)
|
||||
|
||||
for field, attr in _POSSIBLE_DYNAMIC_FIELDS.items():
|
||||
if (val := getattr(self, attr, None)) and not is_static(val):
|
||||
write_field('Dynamic', field)
|
||||
|
||||
long_description = self.get_long_description()
|
||||
if long_description:
|
||||
file.write(f"\n{long_description}")
|
||||
if not long_description.endswith("\n"):
|
||||
file.write("\n")
|
||||
|
||||
|
||||
def _write_requirements(self, file):
|
||||
for req in _reqs.parse(self.install_requires):
|
||||
file.write(f"Requires-Dist: {req}\n")
|
||||
|
||||
processed_extras = {}
|
||||
for augmented_extra, reqs in self.extras_require.items():
|
||||
# Historically, setuptools allows "augmented extras": `<extra>:<condition>`
|
||||
unsafe_extra, _, condition = augmented_extra.partition(":")
|
||||
unsafe_extra = unsafe_extra.strip()
|
||||
extra = _normalization.safe_extra(unsafe_extra)
|
||||
|
||||
if extra:
|
||||
_write_provides_extra(file, processed_extras, extra, unsafe_extra)
|
||||
for req in _reqs.parse_strings(reqs):
|
||||
r = _include_extra(req, extra, condition.strip())
|
||||
file.write(f"Requires-Dist: {r}\n")
|
||||
|
||||
return processed_extras
|
||||
|
||||
|
||||
def _include_extra(req: str, extra: str, condition: str) -> Requirement:
|
||||
r = Requirement(req) # create a fresh object that can be modified
|
||||
parts = (
|
||||
f"({r.marker})" if r.marker else None,
|
||||
f"({condition})" if condition else None,
|
||||
f"extra == {extra!r}" if extra else None,
|
||||
)
|
||||
r.marker = Marker(" and ".join(x for x in parts if x))
|
||||
return r
|
||||
|
||||
|
||||
def _write_provides_extra(file, processed_extras, safe, unsafe):
|
||||
previous = processed_extras.get(safe)
|
||||
if previous == unsafe:
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
'Ambiguity during "extra" normalization for dependencies.',
|
||||
f"""
|
||||
{previous!r} and {unsafe!r} normalize to the same value:\n
|
||||
{safe!r}\n
|
||||
In future versions, setuptools might halt the build process.
|
||||
""",
|
||||
see_url="https://peps.python.org/pep-0685/",
|
||||
)
|
||||
else:
|
||||
processed_extras[safe] = unsafe
|
||||
file.write(f"Provides-Extra: {safe}\n")
|
||||
|
||||
|
||||
# from pypa/distutils#244; needed only until that logic is always available
|
||||
def get_fullname(self):
|
||||
return _distribution_fullname(self.get_name(), self.get_version())
|
||||
|
||||
|
||||
def _distribution_fullname(name: str, version: str) -> str:
|
||||
"""
|
||||
>>> _distribution_fullname('setup.tools', '1.0-2')
|
||||
'setup_tools-1.0.post2'
|
||||
>>> _distribution_fullname('setup-tools', '1.2post2')
|
||||
'setup_tools-1.2.post2'
|
||||
>>> _distribution_fullname('setup-tools', '1.0-r2')
|
||||
'setup_tools-1.0.post2'
|
||||
>>> _distribution_fullname('setup.tools', '1.0.post')
|
||||
'setup_tools-1.0.post0'
|
||||
>>> _distribution_fullname('setup.tools', '1.0+ubuntu-1')
|
||||
'setup_tools-1.0+ubuntu.1'
|
||||
"""
|
||||
return "{}-{}".format(
|
||||
canonicalize_name(name).replace('-', '_'),
|
||||
canonicalize_version(version, strip_trailing_zero=False),
|
||||
)
|
||||
|
||||
|
||||
def _safe_license_file(file):
|
||||
# XXX: Do we need this after the deprecation discussed in #4892, #4896??
|
||||
normalized = os.path.normpath(file).replace(os.sep, "/")
|
||||
if "../" in normalized:
|
||||
return os.path.basename(normalized) # Temporarily restore pre PEP639 behaviour
|
||||
return normalized
|
||||
|
||||
|
||||
_POSSIBLE_DYNAMIC_FIELDS = {
|
||||
# Core Metadata Field x related Distribution attribute
|
||||
"author": "author",
|
||||
"author-email": "author_email",
|
||||
"classifier": "classifiers",
|
||||
"description": "long_description",
|
||||
"description-content-type": "long_description_content_type",
|
||||
"download-url": "download_url",
|
||||
"home-page": "url",
|
||||
"keywords": "keywords",
|
||||
"license": "license",
|
||||
# XXX: License-File is complicated because the user gives globs that are expanded
|
||||
# during the build. Without special handling it is likely always
|
||||
# marked as Dynamic, which is an acceptable outcome according to:
|
||||
# https://github.com/pypa/setuptools/issues/4629#issuecomment-2331233677
|
||||
"license-file": "license_files",
|
||||
"license-expression": "license_expression", # PEP 639
|
||||
"maintainer": "maintainer",
|
||||
"maintainer-email": "maintainer_email",
|
||||
"obsoletes": "obsoletes",
|
||||
# "obsoletes-dist": "obsoletes_dist", # NOT USED
|
||||
"platform": "platforms",
|
||||
"project-url": "project_urls",
|
||||
"provides": "provides",
|
||||
# "provides-dist": "provides_dist", # NOT USED
|
||||
"provides-extra": "extras_require",
|
||||
"requires": "requires",
|
||||
"requires-dist": "install_requires",
|
||||
# "requires-external": "requires_external", # NOT USED
|
||||
"requires-python": "python_requires",
|
||||
"summary": "description",
|
||||
# "supported-platform": "supported_platforms", # NOT USED
|
||||
}
|
||||
14
.venv/Lib/site-packages/setuptools/_distutils/__init__.py
Normal file
14
.venv/Lib/site-packages/setuptools/_distutils/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
__version__, _, _ = sys.version.partition(' ')
|
||||
|
||||
|
||||
try:
|
||||
# Allow Debian and pkgsrc (only) to customize system
|
||||
# behavior. Ref pypa/distutils#2 and pypa/distutils#16.
|
||||
# This hook is deprecated and no other environments
|
||||
# should use it.
|
||||
importlib.import_module('_distutils_system_mod')
|
||||
except ImportError:
|
||||
pass
|
||||
3
.venv/Lib/site-packages/setuptools/_distutils/_log.py
Normal file
3
.venv/Lib/site-packages/setuptools/_distutils/_log.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
@@ -0,0 +1,12 @@
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
|
||||
def bypass_compiler_fixup(cmd, args):
|
||||
return cmd
|
||||
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
compiler_fixup = importlib.import_module('_osx_support').compiler_fixup
|
||||
else:
|
||||
compiler_fixup = bypass_compiler_fixup
|
||||
95
.venv/Lib/site-packages/setuptools/_distutils/_modified.py
Normal file
95
.venv/Lib/site-packages/setuptools/_distutils/_modified.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Timestamp comparison of files and groups of files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os.path
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Literal, TypeVar
|
||||
|
||||
from jaraco.functools import splat
|
||||
|
||||
from .compat.py39 import zip_strict
|
||||
from .errors import DistutilsFileError
|
||||
|
||||
_SourcesT = TypeVar(
|
||||
"_SourcesT", bound="str | bytes | os.PathLike[str] | os.PathLike[bytes]"
|
||||
)
|
||||
_TargetsT = TypeVar(
|
||||
"_TargetsT", bound="str | bytes | os.PathLike[str] | os.PathLike[bytes]"
|
||||
)
|
||||
|
||||
|
||||
def _newer(source, target):
|
||||
return not os.path.exists(target) or (
|
||||
os.path.getmtime(source) > os.path.getmtime(target)
|
||||
)
|
||||
|
||||
|
||||
def newer(
|
||||
source: str | bytes | os.PathLike[str] | os.PathLike[bytes],
|
||||
target: str | bytes | os.PathLike[str] | os.PathLike[bytes],
|
||||
) -> bool:
|
||||
"""
|
||||
Is source modified more recently than target.
|
||||
|
||||
Returns True if 'source' is modified more recently than
|
||||
'target' or if 'target' does not exist.
|
||||
|
||||
Raises DistutilsFileError if 'source' does not exist.
|
||||
"""
|
||||
if not os.path.exists(source):
|
||||
raise DistutilsFileError(f"file {os.path.abspath(source)!r} does not exist")
|
||||
|
||||
return _newer(source, target)
|
||||
|
||||
|
||||
def newer_pairwise(
|
||||
sources: Iterable[_SourcesT],
|
||||
targets: Iterable[_TargetsT],
|
||||
newer: Callable[[_SourcesT, _TargetsT], bool] = newer,
|
||||
) -> tuple[list[_SourcesT], list[_TargetsT]]:
|
||||
"""
|
||||
Filter filenames where sources are newer than targets.
|
||||
|
||||
Walk two filename iterables in parallel, testing if each source is newer
|
||||
than its corresponding target. Returns a pair of lists (sources,
|
||||
targets) where source is newer than target, according to the semantics
|
||||
of 'newer()'.
|
||||
"""
|
||||
newer_pairs = filter(splat(newer), zip_strict(sources, targets))
|
||||
return tuple(map(list, zip(*newer_pairs))) or ([], [])
|
||||
|
||||
|
||||
def newer_group(
|
||||
sources: Iterable[str | bytes | os.PathLike[str] | os.PathLike[bytes]],
|
||||
target: str | bytes | os.PathLike[str] | os.PathLike[bytes],
|
||||
missing: Literal["error", "ignore", "newer"] = "error",
|
||||
) -> bool:
|
||||
"""
|
||||
Is target out-of-date with respect to any file in sources.
|
||||
|
||||
Return True if 'target' is out-of-date with respect to any file
|
||||
listed in 'sources'. In other words, if 'target' exists and is newer
|
||||
than every file in 'sources', return False; otherwise return True.
|
||||
``missing`` controls how to handle a missing source file:
|
||||
|
||||
- error (default): allow the ``stat()`` call to fail.
|
||||
- ignore: silently disregard any missing source files.
|
||||
- newer: treat missing source files as "target out of date". This
|
||||
mode is handy in "dry-run" mode: it will pretend to carry out
|
||||
commands that wouldn't work because inputs are missing, but
|
||||
that doesn't matter because dry-run won't run the commands.
|
||||
"""
|
||||
|
||||
def missing_as_newer(source):
|
||||
return missing == 'newer' and not os.path.exists(source)
|
||||
|
||||
ignored = os.path.exists if missing == 'ignore' else None
|
||||
return not os.path.exists(target) or any(
|
||||
missing_as_newer(source) or _newer(source, target)
|
||||
for source in filter(ignored, sources)
|
||||
)
|
||||
|
||||
|
||||
newer_pairwise_group = functools.partial(newer_pairwise, newer=newer_group)
|
||||
@@ -0,0 +1,16 @@
|
||||
import warnings
|
||||
|
||||
from .compilers.C import msvc
|
||||
|
||||
__all__ = ["MSVCCompiler"]
|
||||
|
||||
MSVCCompiler = msvc.Compiler
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == '_get_vc_env':
|
||||
warnings.warn(
|
||||
"_get_vc_env is private; find an alternative (pypa/distutils#340)"
|
||||
)
|
||||
return msvc._get_vc_env
|
||||
raise AttributeError(name)
|
||||
294
.venv/Lib/site-packages/setuptools/_distutils/archive_util.py
Normal file
294
.venv/Lib/site-packages/setuptools/_distutils/archive_util.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""distutils.archive_util
|
||||
|
||||
Utility functions for creating archive files (tarballs, zip files,
|
||||
that sort of thing)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Literal, overload
|
||||
|
||||
try:
|
||||
import zipfile
|
||||
except ImportError:
|
||||
zipfile = None
|
||||
|
||||
|
||||
from ._log import log
|
||||
from .dir_util import mkpath
|
||||
from .errors import DistutilsExecError
|
||||
from .spawn import spawn
|
||||
|
||||
try:
|
||||
from pwd import getpwnam
|
||||
except ImportError:
|
||||
getpwnam = None
|
||||
|
||||
try:
|
||||
from grp import getgrnam
|
||||
except ImportError:
|
||||
getgrnam = None
|
||||
|
||||
|
||||
def _get_gid(name):
|
||||
"""Returns a gid, given a group name."""
|
||||
if getgrnam is None or name is None:
|
||||
return None
|
||||
try:
|
||||
result = getgrnam(name)
|
||||
except KeyError:
|
||||
result = None
|
||||
if result is not None:
|
||||
return result[2]
|
||||
return None
|
||||
|
||||
|
||||
def _get_uid(name):
|
||||
"""Returns an uid, given a user name."""
|
||||
if getpwnam is None or name is None:
|
||||
return None
|
||||
try:
|
||||
result = getpwnam(name)
|
||||
except KeyError:
|
||||
result = None
|
||||
if result is not None:
|
||||
return result[2]
|
||||
return None
|
||||
|
||||
|
||||
def make_tarball(
|
||||
base_name: str,
|
||||
base_dir: str | os.PathLike[str],
|
||||
compress: Literal["gzip", "bzip2", "xz"] | None = "gzip",
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> str:
|
||||
"""Create a (possibly compressed) tar file from all the files under
|
||||
'base_dir'.
|
||||
|
||||
'compress' must be "gzip" (the default), "bzip2", "xz", or None.
|
||||
|
||||
'owner' and 'group' can be used to define an owner and a group for the
|
||||
archive that is being built. If not provided, the current owner and group
|
||||
will be used.
|
||||
|
||||
The output tar file will be named 'base_dir' + ".tar", possibly plus
|
||||
the appropriate compression extension (".gz", ".bz2", ".xz" or ".Z").
|
||||
|
||||
Returns the output filename.
|
||||
"""
|
||||
tar_compression = {
|
||||
'gzip': 'gz',
|
||||
'bzip2': 'bz2',
|
||||
'xz': 'xz',
|
||||
None: '',
|
||||
}
|
||||
compress_ext = {'gzip': '.gz', 'bzip2': '.bz2', 'xz': '.xz'}
|
||||
|
||||
# flags for compression program, each element of list will be an argument
|
||||
if compress is not None and compress not in compress_ext.keys():
|
||||
raise ValueError(
|
||||
"bad value for 'compress': must be None, 'gzip', 'bzip2', 'xz'"
|
||||
)
|
||||
|
||||
archive_name = base_name + '.tar'
|
||||
archive_name += compress_ext.get(compress, '')
|
||||
|
||||
mkpath(os.path.dirname(archive_name), dry_run=dry_run)
|
||||
|
||||
# creating the tarball
|
||||
import tarfile # late import so Python build itself doesn't break
|
||||
|
||||
log.info('Creating tar archive')
|
||||
|
||||
uid = _get_uid(owner)
|
||||
gid = _get_gid(group)
|
||||
|
||||
def _set_uid_gid(tarinfo):
|
||||
if gid is not None:
|
||||
tarinfo.gid = gid
|
||||
tarinfo.gname = group
|
||||
if uid is not None:
|
||||
tarinfo.uid = uid
|
||||
tarinfo.uname = owner
|
||||
return tarinfo
|
||||
|
||||
if not dry_run:
|
||||
tar = tarfile.open(archive_name, f'w|{tar_compression[compress]}')
|
||||
try:
|
||||
tar.add(base_dir, filter=_set_uid_gid)
|
||||
finally:
|
||||
tar.close()
|
||||
|
||||
return archive_name
|
||||
|
||||
|
||||
def make_zipfile( # noqa: C901
|
||||
base_name: str,
|
||||
base_dir: str | os.PathLike[str],
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False,
|
||||
) -> str:
|
||||
"""Create a zip file from all the files under 'base_dir'.
|
||||
|
||||
The output zip file will be named 'base_name' + ".zip". Uses either the
|
||||
"zipfile" Python module (if available) or the InfoZIP "zip" utility
|
||||
(if installed and found on the default search path). If neither tool is
|
||||
available, raises DistutilsExecError. Returns the name of the output zip
|
||||
file.
|
||||
"""
|
||||
zip_filename = base_name + ".zip"
|
||||
mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
|
||||
|
||||
# If zipfile module is not available, try spawning an external
|
||||
# 'zip' command.
|
||||
if zipfile is None:
|
||||
if verbose:
|
||||
zipoptions = "-r"
|
||||
else:
|
||||
zipoptions = "-rq"
|
||||
|
||||
try:
|
||||
spawn(["zip", zipoptions, zip_filename, base_dir], dry_run=dry_run)
|
||||
except DistutilsExecError:
|
||||
# XXX really should distinguish between "couldn't find
|
||||
# external 'zip' command" and "zip failed".
|
||||
raise DistutilsExecError(
|
||||
f"unable to create zip file '{zip_filename}': "
|
||||
"could neither import the 'zipfile' module nor "
|
||||
"find a standalone zip utility"
|
||||
)
|
||||
|
||||
else:
|
||||
log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
|
||||
|
||||
if not dry_run:
|
||||
try:
|
||||
zip = zipfile.ZipFile(
|
||||
zip_filename, "w", compression=zipfile.ZIP_DEFLATED
|
||||
)
|
||||
except RuntimeError:
|
||||
zip = zipfile.ZipFile(zip_filename, "w", compression=zipfile.ZIP_STORED)
|
||||
|
||||
with zip:
|
||||
if base_dir != os.curdir:
|
||||
path = os.path.normpath(os.path.join(base_dir, ''))
|
||||
zip.write(path, path)
|
||||
log.info("adding '%s'", path)
|
||||
for dirpath, dirnames, filenames in os.walk(base_dir):
|
||||
for name in dirnames:
|
||||
path = os.path.normpath(os.path.join(dirpath, name, ''))
|
||||
zip.write(path, path)
|
||||
log.info("adding '%s'", path)
|
||||
for name in filenames:
|
||||
path = os.path.normpath(os.path.join(dirpath, name))
|
||||
if os.path.isfile(path):
|
||||
zip.write(path, path)
|
||||
log.info("adding '%s'", path)
|
||||
|
||||
return zip_filename
|
||||
|
||||
|
||||
ARCHIVE_FORMATS = {
|
||||
'gztar': (make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"),
|
||||
'bztar': (make_tarball, [('compress', 'bzip2')], "bzip2'ed tar-file"),
|
||||
'xztar': (make_tarball, [('compress', 'xz')], "xz'ed tar-file"),
|
||||
'ztar': (make_tarball, [('compress', 'compress')], "compressed tar file"),
|
||||
'tar': (make_tarball, [('compress', None)], "uncompressed tar file"),
|
||||
'zip': (make_zipfile, [], "ZIP file"),
|
||||
}
|
||||
|
||||
|
||||
def check_archive_formats(formats):
|
||||
"""Returns the first format from the 'format' list that is unknown.
|
||||
|
||||
If all formats are known, returns None
|
||||
"""
|
||||
for format in formats:
|
||||
if format not in ARCHIVE_FORMATS:
|
||||
return format
|
||||
return None
|
||||
|
||||
|
||||
@overload
|
||||
def make_archive(
|
||||
base_name: str,
|
||||
format: str,
|
||||
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
|
||||
base_dir: str | None = None,
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> str: ...
|
||||
@overload
|
||||
def make_archive(
|
||||
base_name: str | os.PathLike[str],
|
||||
format: str,
|
||||
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes],
|
||||
base_dir: str | None = None,
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> str: ...
|
||||
def make_archive(
|
||||
base_name: str | os.PathLike[str],
|
||||
format: str,
|
||||
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
|
||||
base_dir: str | None = None,
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> str:
|
||||
"""Create an archive file (eg. zip or tar).
|
||||
|
||||
'base_name' is the name of the file to create, minus any format-specific
|
||||
extension; 'format' is the archive format: one of "zip", "tar", "gztar",
|
||||
"bztar", "xztar", or "ztar".
|
||||
|
||||
'root_dir' is a directory that will be the root directory of the
|
||||
archive; ie. we typically chdir into 'root_dir' before creating the
|
||||
archive. 'base_dir' is the directory where we start archiving from;
|
||||
ie. 'base_dir' will be the common prefix of all files and
|
||||
directories in the archive. 'root_dir' and 'base_dir' both default
|
||||
to the current directory. Returns the name of the archive file.
|
||||
|
||||
'owner' and 'group' are used when creating a tar archive. By default,
|
||||
uses the current owner and group.
|
||||
"""
|
||||
save_cwd = os.getcwd()
|
||||
if root_dir is not None:
|
||||
log.debug("changing into '%s'", root_dir)
|
||||
base_name = os.path.abspath(base_name)
|
||||
if not dry_run:
|
||||
os.chdir(root_dir)
|
||||
|
||||
if base_dir is None:
|
||||
base_dir = os.curdir
|
||||
|
||||
kwargs = {'dry_run': dry_run}
|
||||
|
||||
try:
|
||||
format_info = ARCHIVE_FORMATS[format]
|
||||
except KeyError:
|
||||
raise ValueError(f"unknown archive format '{format}'")
|
||||
|
||||
func = format_info[0]
|
||||
kwargs.update(format_info[1])
|
||||
|
||||
if format != 'zip':
|
||||
kwargs['owner'] = owner
|
||||
kwargs['group'] = group
|
||||
|
||||
try:
|
||||
filename = func(base_name, base_dir, **kwargs)
|
||||
finally:
|
||||
if root_dir is not None:
|
||||
log.debug("changing back to '%s'", save_cwd)
|
||||
os.chdir(save_cwd)
|
||||
|
||||
return filename
|
||||
26
.venv/Lib/site-packages/setuptools/_distutils/ccompiler.py
Normal file
26
.venv/Lib/site-packages/setuptools/_distutils/ccompiler.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from .compat.numpy import ( # noqa: F401
|
||||
_default_compilers,
|
||||
compiler_class,
|
||||
)
|
||||
from .compilers.C import base
|
||||
from .compilers.C.base import (
|
||||
gen_lib_options,
|
||||
gen_preprocess_options,
|
||||
get_default_compiler,
|
||||
new_compiler,
|
||||
show_compilers,
|
||||
)
|
||||
from .compilers.C.errors import CompileError, LinkError
|
||||
|
||||
__all__ = [
|
||||
'CompileError',
|
||||
'LinkError',
|
||||
'gen_lib_options',
|
||||
'gen_preprocess_options',
|
||||
'get_default_compiler',
|
||||
'new_compiler',
|
||||
'show_compilers',
|
||||
]
|
||||
|
||||
|
||||
CCompiler = base.Compiler
|
||||
554
.venv/Lib/site-packages/setuptools/_distutils/cmd.py
Normal file
554
.venv/Lib/site-packages/setuptools/_distutils/cmd.py
Normal file
@@ -0,0 +1,554 @@
|
||||
"""distutils.cmd
|
||||
|
||||
Provides the Command class, the base class for the command classes
|
||||
in the distutils.command package.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, MutableSequence
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload
|
||||
|
||||
from . import _modified, archive_util, dir_util, file_util, util
|
||||
from ._log import log
|
||||
from .errors import DistutilsOptionError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# type-only import because of mutual dependence between these classes
|
||||
from distutils.dist import Distribution
|
||||
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
_Ts = TypeVarTuple("_Ts")
|
||||
|
||||
_StrPathT = TypeVar("_StrPathT", bound="str | os.PathLike[str]")
|
||||
_BytesPathT = TypeVar("_BytesPathT", bound="bytes | os.PathLike[bytes]")
|
||||
_CommandT = TypeVar("_CommandT", bound="Command")
|
||||
|
||||
|
||||
class Command:
|
||||
"""Abstract base class for defining command classes, the "worker bees"
|
||||
of the Distutils. A useful analogy for command classes is to think of
|
||||
them as subroutines with local variables called "options". The options
|
||||
are "declared" in 'initialize_options()' and "defined" (given their
|
||||
final values, aka "finalized") in 'finalize_options()', both of which
|
||||
must be defined by every command class. The distinction between the
|
||||
two is necessary because option values might come from the outside
|
||||
world (command line, config file, ...), and any options dependent on
|
||||
other options must be computed *after* these outside influences have
|
||||
been processed -- hence 'finalize_options()'. The "body" of the
|
||||
subroutine, where it does all its work based on the values of its
|
||||
options, is the 'run()' method, which must also be implemented by every
|
||||
command class.
|
||||
"""
|
||||
|
||||
# 'sub_commands' formalizes the notion of a "family" of commands,
|
||||
# eg. "install" as the parent with sub-commands "install_lib",
|
||||
# "install_headers", etc. The parent of a family of commands
|
||||
# defines 'sub_commands' as a class attribute; it's a list of
|
||||
# (command_name : string, predicate : unbound_method | string | None)
|
||||
# tuples, where 'predicate' is a method of the parent command that
|
||||
# determines whether the corresponding command is applicable in the
|
||||
# current situation. (Eg. we "install_headers" is only applicable if
|
||||
# we have any C header files to install.) If 'predicate' is None,
|
||||
# that command is always applicable.
|
||||
#
|
||||
# 'sub_commands' is usually defined at the *end* of a class, because
|
||||
# predicates can be unbound methods, so they must already have been
|
||||
# defined. The canonical example is the "install" command.
|
||||
sub_commands: ClassVar[ # Any to work around variance issues
|
||||
list[tuple[str, Callable[[Any], bool] | None]]
|
||||
] = []
|
||||
|
||||
user_options: ClassVar[
|
||||
# Specifying both because list is invariant. Avoids mypy override assignment issues
|
||||
list[tuple[str, str, str]] | list[tuple[str, str | None, str]]
|
||||
] = []
|
||||
|
||||
# -- Creation/initialization methods -------------------------------
|
||||
|
||||
def __init__(self, dist: Distribution) -> None:
|
||||
"""Create and initialize a new Command object. Most importantly,
|
||||
invokes the 'initialize_options()' method, which is the real
|
||||
initializer and depends on the actual command being
|
||||
instantiated.
|
||||
"""
|
||||
# late import because of mutual dependence between these classes
|
||||
from distutils.dist import Distribution
|
||||
|
||||
if not isinstance(dist, Distribution):
|
||||
raise TypeError("dist must be a Distribution instance")
|
||||
if self.__class__ is Command:
|
||||
raise RuntimeError("Command is an abstract class")
|
||||
|
||||
self.distribution = dist
|
||||
self.initialize_options()
|
||||
|
||||
# Per-command versions of the global flags, so that the user can
|
||||
# customize Distutils' behaviour command-by-command and let some
|
||||
# commands fall back on the Distribution's behaviour. None means
|
||||
# "not defined, check self.distribution's copy", while 0 or 1 mean
|
||||
# false and true (duh). Note that this means figuring out the real
|
||||
# value of each flag is a touch complicated -- hence "self._dry_run"
|
||||
# will be handled by __getattr__, below.
|
||||
# XXX This needs to be fixed.
|
||||
self._dry_run = None
|
||||
|
||||
# verbose is largely ignored, but needs to be set for
|
||||
# backwards compatibility (I think)?
|
||||
self.verbose = dist.verbose
|
||||
|
||||
# Some commands define a 'self.force' option to ignore file
|
||||
# timestamps, but methods defined *here* assume that
|
||||
# 'self.force' exists for all commands. So define it here
|
||||
# just to be safe.
|
||||
self.force = None
|
||||
|
||||
# The 'help' flag is just used for command-line parsing, so
|
||||
# none of that complicated bureaucracy is needed.
|
||||
self.help = False
|
||||
|
||||
# 'finalized' records whether or not 'finalize_options()' has been
|
||||
# called. 'finalize_options()' itself should not pay attention to
|
||||
# this flag: it is the business of 'ensure_finalized()', which
|
||||
# always calls 'finalize_options()', to respect/update it.
|
||||
self.finalized = False
|
||||
|
||||
# XXX A more explicit way to customize dry_run would be better.
|
||||
def __getattr__(self, attr):
|
||||
if attr == 'dry_run':
|
||||
myval = getattr(self, "_" + attr)
|
||||
if myval is None:
|
||||
return getattr(self.distribution, attr)
|
||||
else:
|
||||
return myval
|
||||
else:
|
||||
raise AttributeError(attr)
|
||||
|
||||
def ensure_finalized(self) -> None:
|
||||
if not self.finalized:
|
||||
self.finalize_options()
|
||||
self.finalized = True
|
||||
|
||||
# Subclasses must define:
|
||||
# initialize_options()
|
||||
# provide default values for all options; may be customized by
|
||||
# setup script, by options from config file(s), or by command-line
|
||||
# options
|
||||
# finalize_options()
|
||||
# decide on the final values for all options; this is called
|
||||
# after all possible intervention from the outside world
|
||||
# (command-line, option file, etc.) has been processed
|
||||
# run()
|
||||
# run the command: do whatever it is we're here to do,
|
||||
# controlled by the command's various option values
|
||||
|
||||
@abstractmethod
|
||||
def initialize_options(self) -> None:
|
||||
"""Set default values for all the options that this command
|
||||
supports. Note that these defaults may be overridden by other
|
||||
commands, by the setup script, by config files, or by the
|
||||
command-line. Thus, this is not the place to code dependencies
|
||||
between options; generally, 'initialize_options()' implementations
|
||||
are just a bunch of "self.foo = None" assignments.
|
||||
|
||||
This method must be implemented by all command classes.
|
||||
"""
|
||||
raise RuntimeError(
|
||||
f"abstract method -- subclass {self.__class__} must override"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def finalize_options(self) -> None:
|
||||
"""Set final values for all the options that this command supports.
|
||||
This is always called as late as possible, ie. after any option
|
||||
assignments from the command-line or from other commands have been
|
||||
done. Thus, this is the place to code option dependencies: if
|
||||
'foo' depends on 'bar', then it is safe to set 'foo' from 'bar' as
|
||||
long as 'foo' still has the same value it was assigned in
|
||||
'initialize_options()'.
|
||||
|
||||
This method must be implemented by all command classes.
|
||||
"""
|
||||
raise RuntimeError(
|
||||
f"abstract method -- subclass {self.__class__} must override"
|
||||
)
|
||||
|
||||
def dump_options(self, header=None, indent=""):
|
||||
from distutils.fancy_getopt import longopt_xlate
|
||||
|
||||
if header is None:
|
||||
header = f"command options for '{self.get_command_name()}':"
|
||||
self.announce(indent + header, level=logging.INFO)
|
||||
indent = indent + " "
|
||||
for option, _, _ in self.user_options:
|
||||
option = option.translate(longopt_xlate)
|
||||
if option[-1] == "=":
|
||||
option = option[:-1]
|
||||
value = getattr(self, option)
|
||||
self.announce(indent + f"{option} = {value}", level=logging.INFO)
|
||||
|
||||
@abstractmethod
|
||||
def run(self) -> None:
|
||||
"""A command's raison d'etre: carry out the action it exists to
|
||||
perform, controlled by the options initialized in
|
||||
'initialize_options()', customized by other commands, the setup
|
||||
script, the command-line, and config files, and finalized in
|
||||
'finalize_options()'. All terminal output and filesystem
|
||||
interaction should be done by 'run()'.
|
||||
|
||||
This method must be implemented by all command classes.
|
||||
"""
|
||||
raise RuntimeError(
|
||||
f"abstract method -- subclass {self.__class__} must override"
|
||||
)
|
||||
|
||||
def announce(self, msg: object, level: int = logging.DEBUG) -> None:
|
||||
log.log(level, msg)
|
||||
|
||||
def debug_print(self, msg: object) -> None:
|
||||
"""Print 'msg' to stdout if the global DEBUG (taken from the
|
||||
DISTUTILS_DEBUG environment variable) flag is true.
|
||||
"""
|
||||
from distutils.debug import DEBUG
|
||||
|
||||
if DEBUG:
|
||||
print(msg)
|
||||
sys.stdout.flush()
|
||||
|
||||
# -- Option validation methods -------------------------------------
|
||||
# (these are very handy in writing the 'finalize_options()' method)
|
||||
#
|
||||
# NB. the general philosophy here is to ensure that a particular option
|
||||
# value meets certain type and value constraints. If not, we try to
|
||||
# force it into conformance (eg. if we expect a list but have a string,
|
||||
# split the string on comma and/or whitespace). If we can't force the
|
||||
# option into conformance, raise DistutilsOptionError. Thus, command
|
||||
# classes need do nothing more than (eg.)
|
||||
# self.ensure_string_list('foo')
|
||||
# and they can be guaranteed that thereafter, self.foo will be
|
||||
# a list of strings.
|
||||
|
||||
def _ensure_stringlike(self, option, what, default=None):
|
||||
val = getattr(self, option)
|
||||
if val is None:
|
||||
setattr(self, option, default)
|
||||
return default
|
||||
elif not isinstance(val, str):
|
||||
raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)")
|
||||
return val
|
||||
|
||||
def ensure_string(self, option: str, default: str | None = None) -> None:
|
||||
"""Ensure that 'option' is a string; if not defined, set it to
|
||||
'default'.
|
||||
"""
|
||||
self._ensure_stringlike(option, "string", default)
|
||||
|
||||
def ensure_string_list(self, option: str) -> None:
|
||||
r"""Ensure that 'option' is a list of strings. If 'option' is
|
||||
currently a string, we split it either on /,\s*/ or /\s+/, so
|
||||
"foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
|
||||
["foo", "bar", "baz"].
|
||||
"""
|
||||
val = getattr(self, option)
|
||||
if val is None:
|
||||
return
|
||||
elif isinstance(val, str):
|
||||
setattr(self, option, re.split(r',\s*|\s+', val))
|
||||
else:
|
||||
if isinstance(val, list):
|
||||
ok = all(isinstance(v, str) for v in val)
|
||||
else:
|
||||
ok = False
|
||||
if not ok:
|
||||
raise DistutilsOptionError(
|
||||
f"'{option}' must be a list of strings (got {val!r})"
|
||||
)
|
||||
|
||||
def _ensure_tested_string(self, option, tester, what, error_fmt, default=None):
|
||||
val = self._ensure_stringlike(option, what, default)
|
||||
if val is not None and not tester(val):
|
||||
raise DistutilsOptionError(
|
||||
("error in '%s' option: " + error_fmt) % (option, val)
|
||||
)
|
||||
|
||||
def ensure_filename(self, option: str) -> None:
|
||||
"""Ensure that 'option' is the name of an existing file."""
|
||||
self._ensure_tested_string(
|
||||
option, os.path.isfile, "filename", "'%s' does not exist or is not a file"
|
||||
)
|
||||
|
||||
def ensure_dirname(self, option: str) -> None:
|
||||
self._ensure_tested_string(
|
||||
option,
|
||||
os.path.isdir,
|
||||
"directory name",
|
||||
"'%s' does not exist or is not a directory",
|
||||
)
|
||||
|
||||
# -- Convenience methods for commands ------------------------------
|
||||
|
||||
def get_command_name(self) -> str:
|
||||
if hasattr(self, 'command_name'):
|
||||
return self.command_name
|
||||
else:
|
||||
return self.__class__.__name__
|
||||
|
||||
def set_undefined_options(
|
||||
self, src_cmd: str, *option_pairs: tuple[str, str]
|
||||
) -> None:
|
||||
"""Set the values of any "undefined" options from corresponding
|
||||
option values in some other command object. "Undefined" here means
|
||||
"is None", which is the convention used to indicate that an option
|
||||
has not been changed between 'initialize_options()' and
|
||||
'finalize_options()'. Usually called from 'finalize_options()' for
|
||||
options that depend on some other command rather than another
|
||||
option of the same command. 'src_cmd' is the other command from
|
||||
which option values will be taken (a command object will be created
|
||||
for it if necessary); the remaining arguments are
|
||||
'(src_option,dst_option)' tuples which mean "take the value of
|
||||
'src_option' in the 'src_cmd' command object, and copy it to
|
||||
'dst_option' in the current command object".
|
||||
"""
|
||||
# Option_pairs: list of (src_option, dst_option) tuples
|
||||
src_cmd_obj = self.distribution.get_command_obj(src_cmd)
|
||||
src_cmd_obj.ensure_finalized()
|
||||
for src_option, dst_option in option_pairs:
|
||||
if getattr(self, dst_option) is None:
|
||||
setattr(self, dst_option, getattr(src_cmd_obj, src_option))
|
||||
|
||||
# NOTE: Because distutils is private to Setuptools and not all commands are exposed here,
|
||||
# not every possible command is enumerated in the signature.
|
||||
def get_finalized_command(self, command: str, create: bool = True) -> Command:
|
||||
"""Wrapper around Distribution's 'get_command_obj()' method: find
|
||||
(create if necessary and 'create' is true) the command object for
|
||||
'command', call its 'ensure_finalized()' method, and return the
|
||||
finalized command object.
|
||||
"""
|
||||
cmd_obj = self.distribution.get_command_obj(command, create)
|
||||
cmd_obj.ensure_finalized()
|
||||
return cmd_obj
|
||||
|
||||
# XXX rename to 'get_reinitialized_command()'? (should do the
|
||||
# same in dist.py, if so)
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: str, reinit_subcommands: bool = False
|
||||
) -> Command: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: _CommandT, reinit_subcommands: bool = False
|
||||
) -> _CommandT: ...
|
||||
def reinitialize_command(
|
||||
self, command: str | Command, reinit_subcommands=False
|
||||
) -> Command:
|
||||
return self.distribution.reinitialize_command(command, reinit_subcommands)
|
||||
|
||||
def run_command(self, command: str) -> None:
|
||||
"""Run some other command: uses the 'run_command()' method of
|
||||
Distribution, which creates and finalizes the command object if
|
||||
necessary and then invokes its 'run()' method.
|
||||
"""
|
||||
self.distribution.run_command(command)
|
||||
|
||||
def get_sub_commands(self) -> list[str]:
|
||||
"""Determine the sub-commands that are relevant in the current
|
||||
distribution (ie., that need to be run). This is based on the
|
||||
'sub_commands' class attribute: each tuple in that list may include
|
||||
a method that we call to determine if the subcommand needs to be
|
||||
run for the current distribution. Return a list of command names.
|
||||
"""
|
||||
commands = []
|
||||
for cmd_name, method in self.sub_commands:
|
||||
if method is None or method(self):
|
||||
commands.append(cmd_name)
|
||||
return commands
|
||||
|
||||
# -- External world manipulation -----------------------------------
|
||||
|
||||
def warn(self, msg: object) -> None:
|
||||
log.warning("warning: %s: %s\n", self.get_command_name(), msg)
|
||||
|
||||
def execute(
|
||||
self,
|
||||
func: Callable[[Unpack[_Ts]], object],
|
||||
args: tuple[Unpack[_Ts]],
|
||||
msg: object = None,
|
||||
level: int = 1,
|
||||
) -> None:
|
||||
util.execute(func, args, msg, dry_run=self.dry_run)
|
||||
|
||||
def mkpath(self, name: str, mode: int = 0o777) -> None:
|
||||
dir_util.mkpath(name, mode, dry_run=self.dry_run)
|
||||
|
||||
@overload
|
||||
def copy_file(
|
||||
self,
|
||||
infile: str | os.PathLike[str],
|
||||
outfile: _StrPathT,
|
||||
preserve_mode: bool = True,
|
||||
preserve_times: bool = True,
|
||||
link: str | None = None,
|
||||
level: int = 1,
|
||||
) -> tuple[_StrPathT | str, bool]: ...
|
||||
@overload
|
||||
def copy_file(
|
||||
self,
|
||||
infile: bytes | os.PathLike[bytes],
|
||||
outfile: _BytesPathT,
|
||||
preserve_mode: bool = True,
|
||||
preserve_times: bool = True,
|
||||
link: str | None = None,
|
||||
level: int = 1,
|
||||
) -> tuple[_BytesPathT | bytes, bool]: ...
|
||||
def copy_file(
|
||||
self,
|
||||
infile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
|
||||
outfile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
|
||||
preserve_mode: bool = True,
|
||||
preserve_times: bool = True,
|
||||
link: str | None = None,
|
||||
level: int = 1,
|
||||
) -> tuple[str | os.PathLike[str] | bytes | os.PathLike[bytes], bool]:
|
||||
"""Copy a file respecting verbose, dry-run and force flags. (The
|
||||
former two default to whatever is in the Distribution object, and
|
||||
the latter defaults to false for commands that don't define it.)"""
|
||||
return file_util.copy_file(
|
||||
infile,
|
||||
outfile,
|
||||
preserve_mode,
|
||||
preserve_times,
|
||||
not self.force,
|
||||
link,
|
||||
dry_run=self.dry_run,
|
||||
)
|
||||
|
||||
def copy_tree(
|
||||
self,
|
||||
infile: str | os.PathLike[str],
|
||||
outfile: str,
|
||||
preserve_mode: bool = True,
|
||||
preserve_times: bool = True,
|
||||
preserve_symlinks: bool = False,
|
||||
level: int = 1,
|
||||
) -> list[str]:
|
||||
"""Copy an entire directory tree respecting verbose, dry-run,
|
||||
and force flags.
|
||||
"""
|
||||
return dir_util.copy_tree(
|
||||
infile,
|
||||
outfile,
|
||||
preserve_mode,
|
||||
preserve_times,
|
||||
preserve_symlinks,
|
||||
not self.force,
|
||||
dry_run=self.dry_run,
|
||||
)
|
||||
|
||||
@overload
|
||||
def move_file(
|
||||
self, src: str | os.PathLike[str], dst: _StrPathT, level: int = 1
|
||||
) -> _StrPathT | str: ...
|
||||
@overload
|
||||
def move_file(
|
||||
self, src: bytes | os.PathLike[bytes], dst: _BytesPathT, level: int = 1
|
||||
) -> _BytesPathT | bytes: ...
|
||||
def move_file(
|
||||
self,
|
||||
src: str | os.PathLike[str] | bytes | os.PathLike[bytes],
|
||||
dst: str | os.PathLike[str] | bytes | os.PathLike[bytes],
|
||||
level: int = 1,
|
||||
) -> str | os.PathLike[str] | bytes | os.PathLike[bytes]:
|
||||
"""Move a file respecting dry-run flag."""
|
||||
return file_util.move_file(src, dst, dry_run=self.dry_run)
|
||||
|
||||
def spawn(
|
||||
self, cmd: MutableSequence[str], search_path: bool = True, level: int = 1
|
||||
) -> None:
|
||||
"""Spawn an external command respecting dry-run flag."""
|
||||
from distutils.spawn import spawn
|
||||
|
||||
spawn(cmd, search_path, dry_run=self.dry_run)
|
||||
|
||||
@overload
|
||||
def make_archive(
|
||||
self,
|
||||
base_name: str,
|
||||
format: str,
|
||||
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
|
||||
base_dir: str | None = None,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> str: ...
|
||||
@overload
|
||||
def make_archive(
|
||||
self,
|
||||
base_name: str | os.PathLike[str],
|
||||
format: str,
|
||||
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes],
|
||||
base_dir: str | None = None,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> str: ...
|
||||
def make_archive(
|
||||
self,
|
||||
base_name: str | os.PathLike[str],
|
||||
format: str,
|
||||
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
|
||||
base_dir: str | None = None,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> str:
|
||||
return archive_util.make_archive(
|
||||
base_name,
|
||||
format,
|
||||
root_dir,
|
||||
base_dir,
|
||||
dry_run=self.dry_run,
|
||||
owner=owner,
|
||||
group=group,
|
||||
)
|
||||
|
||||
def make_file(
|
||||
self,
|
||||
infiles: str | list[str] | tuple[str, ...],
|
||||
outfile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
|
||||
func: Callable[[Unpack[_Ts]], object],
|
||||
args: tuple[Unpack[_Ts]],
|
||||
exec_msg: object = None,
|
||||
skip_msg: object = None,
|
||||
level: int = 1,
|
||||
) -> None:
|
||||
"""Special case of 'execute()' for operations that process one or
|
||||
more input files and generate one output file. Works just like
|
||||
'execute()', except the operation is skipped and a different
|
||||
message printed if 'outfile' already exists and is newer than all
|
||||
files listed in 'infiles'. If the command defined 'self.force',
|
||||
and it is true, then the command is unconditionally run -- does no
|
||||
timestamp checks.
|
||||
"""
|
||||
if skip_msg is None:
|
||||
skip_msg = f"skipping {outfile} (inputs unchanged)"
|
||||
|
||||
# Allow 'infiles' to be a single string
|
||||
if isinstance(infiles, str):
|
||||
infiles = (infiles,)
|
||||
elif not isinstance(infiles, (list, tuple)):
|
||||
raise TypeError("'infiles' must be a string, or a list or tuple of strings")
|
||||
|
||||
if exec_msg is None:
|
||||
exec_msg = "generating {} from {}".format(outfile, ', '.join(infiles))
|
||||
|
||||
# If 'outfile' must be regenerated (either because it doesn't
|
||||
# exist, is out-of-date, or the 'force' flag is true) then
|
||||
# perform the action that presumably regenerates it
|
||||
if self.force or _modified.newer_group(infiles, outfile):
|
||||
self.execute(func, args, exec_msg, level)
|
||||
# Otherwise, print the "skip" message
|
||||
else:
|
||||
log.debug(skip_msg)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""distutils.command
|
||||
|
||||
Package containing implementation of all the standard Distutils
|
||||
commands."""
|
||||
|
||||
__all__ = [
|
||||
'build',
|
||||
'build_py',
|
||||
'build_ext',
|
||||
'build_clib',
|
||||
'build_scripts',
|
||||
'clean',
|
||||
'install',
|
||||
'install_lib',
|
||||
'install_headers',
|
||||
'install_scripts',
|
||||
'install_data',
|
||||
'sdist',
|
||||
'bdist',
|
||||
'bdist_dumb',
|
||||
'bdist_rpm',
|
||||
'check',
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Backward compatibility for homebrew builds on macOS.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def enabled():
|
||||
"""
|
||||
Only enabled for Python 3.9 framework homebrew builds
|
||||
except ensurepip and venv.
|
||||
"""
|
||||
PY39 = (3, 9) < sys.version_info < (3, 10)
|
||||
framework = sys.platform == 'darwin' and sys._framework
|
||||
homebrew = "Cellar" in sysconfig.get_config_var('projectbase')
|
||||
venv = sys.prefix != sys.base_prefix
|
||||
ensurepip = os.environ.get("ENSUREPIP_OPTIONS")
|
||||
return PY39 and framework and homebrew and not venv and not ensurepip
|
||||
|
||||
|
||||
schemes = dict(
|
||||
osx_framework_library=dict(
|
||||
stdlib='{installed_base}/{platlibdir}/python{py_version_short}',
|
||||
platstdlib='{platbase}/{platlibdir}/python{py_version_short}',
|
||||
purelib='{homebrew_prefix}/lib/python{py_version_short}/site-packages',
|
||||
platlib='{homebrew_prefix}/{platlibdir}/python{py_version_short}/site-packages',
|
||||
include='{installed_base}/include/python{py_version_short}{abiflags}',
|
||||
platinclude='{installed_platbase}/include/python{py_version_short}{abiflags}',
|
||||
scripts='{homebrew_prefix}/bin',
|
||||
data='{homebrew_prefix}',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def vars():
|
||||
if not enabled():
|
||||
return {}
|
||||
homebrew_prefix = subprocess.check_output(['brew', '--prefix'], text=True).strip()
|
||||
return locals()
|
||||
|
||||
|
||||
def scheme(name):
|
||||
"""
|
||||
Override the selected scheme for posix_prefix.
|
||||
"""
|
||||
if not enabled() or not name.endswith('_prefix'):
|
||||
return name
|
||||
return 'osx_framework_library'
|
||||
167
.venv/Lib/site-packages/setuptools/_distutils/command/bdist.py
Normal file
167
.venv/Lib/site-packages/setuptools/_distutils/command/bdist.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""distutils.command.bdist
|
||||
|
||||
Implements the Distutils 'bdist' command (create a built [binary]
|
||||
distribution)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..errors import DistutilsOptionError, DistutilsPlatformError
|
||||
from ..util import get_platform
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import deprecated
|
||||
else:
|
||||
|
||||
def deprecated(message):
|
||||
return lambda fn: fn
|
||||
|
||||
|
||||
def show_formats():
|
||||
"""Print list of available formats (arguments to "--format" option)."""
|
||||
from ..fancy_getopt import FancyGetopt
|
||||
|
||||
formats = [
|
||||
("formats=" + format, None, bdist.format_commands[format][1])
|
||||
for format in bdist.format_commands
|
||||
]
|
||||
pretty_printer = FancyGetopt(formats)
|
||||
pretty_printer.print_help("List of available distribution formats:")
|
||||
|
||||
|
||||
class ListCompat(dict[str, tuple[str, str]]):
|
||||
# adapter to allow for Setuptools compatibility in format_commands
|
||||
@deprecated("format_commands is now a dict. append is deprecated.")
|
||||
def append(self, item: object) -> None:
|
||||
warnings.warn(
|
||||
"format_commands is now a dict. append is deprecated.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
class bdist(Command):
|
||||
description = "create a built (binary) distribution"
|
||||
|
||||
user_options = [
|
||||
('bdist-base=', 'b', "temporary directory for creating built distributions"),
|
||||
(
|
||||
'plat-name=',
|
||||
'p',
|
||||
"platform name to embed in generated filenames "
|
||||
f"[default: {get_platform()}]",
|
||||
),
|
||||
('formats=', None, "formats for distribution (comma-separated list)"),
|
||||
(
|
||||
'dist-dir=',
|
||||
'd',
|
||||
"directory to put final built distributions in [default: dist]",
|
||||
),
|
||||
('skip-build', None, "skip rebuilding everything (for testing/debugging)"),
|
||||
(
|
||||
'owner=',
|
||||
'u',
|
||||
"Owner name used when creating a tar file [default: current user]",
|
||||
),
|
||||
(
|
||||
'group=',
|
||||
'g',
|
||||
"Group name used when creating a tar file [default: current group]",
|
||||
),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['skip-build']
|
||||
|
||||
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
|
||||
('help-formats', None, "lists available distribution formats", show_formats),
|
||||
]
|
||||
|
||||
# The following commands do not take a format option from bdist
|
||||
no_format_option: ClassVar[tuple[str, ...]] = ('bdist_rpm',)
|
||||
|
||||
# This won't do in reality: will need to distinguish RPM-ish Linux,
|
||||
# Debian-ish Linux, Solaris, FreeBSD, ..., Windows, Mac OS.
|
||||
default_format: ClassVar[dict[str, str]] = {'posix': 'gztar', 'nt': 'zip'}
|
||||
|
||||
# Define commands in preferred order for the --help-formats option
|
||||
format_commands = ListCompat({
|
||||
'rpm': ('bdist_rpm', "RPM distribution"),
|
||||
'gztar': ('bdist_dumb', "gzip'ed tar file"),
|
||||
'bztar': ('bdist_dumb', "bzip2'ed tar file"),
|
||||
'xztar': ('bdist_dumb', "xz'ed tar file"),
|
||||
'ztar': ('bdist_dumb', "compressed tar file"),
|
||||
'tar': ('bdist_dumb', "tar file"),
|
||||
'zip': ('bdist_dumb', "ZIP file"),
|
||||
})
|
||||
|
||||
# for compatibility until consumers only reference format_commands
|
||||
format_command = format_commands
|
||||
|
||||
def initialize_options(self):
|
||||
self.bdist_base = None
|
||||
self.plat_name = None
|
||||
self.formats = None
|
||||
self.dist_dir = None
|
||||
self.skip_build = False
|
||||
self.group = None
|
||||
self.owner = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
# have to finalize 'plat_name' before 'bdist_base'
|
||||
if self.plat_name is None:
|
||||
if self.skip_build:
|
||||
self.plat_name = get_platform()
|
||||
else:
|
||||
self.plat_name = self.get_finalized_command('build').plat_name
|
||||
|
||||
# 'bdist_base' -- parent of per-built-distribution-format
|
||||
# temporary directories (eg. we'll probably have
|
||||
# "build/bdist.<plat>/dumb", "build/bdist.<plat>/rpm", etc.)
|
||||
if self.bdist_base is None:
|
||||
build_base = self.get_finalized_command('build').build_base
|
||||
self.bdist_base = os.path.join(build_base, 'bdist.' + self.plat_name)
|
||||
|
||||
self.ensure_string_list('formats')
|
||||
if self.formats is None:
|
||||
try:
|
||||
self.formats = [self.default_format[os.name]]
|
||||
except KeyError:
|
||||
raise DistutilsPlatformError(
|
||||
"don't know how to create built distributions "
|
||||
f"on platform {os.name}"
|
||||
)
|
||||
|
||||
if self.dist_dir is None:
|
||||
self.dist_dir = "dist"
|
||||
|
||||
def run(self) -> None:
|
||||
# Figure out which sub-commands we need to run.
|
||||
commands = []
|
||||
for format in self.formats:
|
||||
try:
|
||||
commands.append(self.format_commands[format][0])
|
||||
except KeyError:
|
||||
raise DistutilsOptionError(f"invalid format '{format}'")
|
||||
|
||||
# Reinitialize and run each command.
|
||||
for i in range(len(self.formats)):
|
||||
cmd_name = commands[i]
|
||||
sub_cmd = self.reinitialize_command(cmd_name)
|
||||
if cmd_name not in self.no_format_option:
|
||||
sub_cmd.format = self.formats[i]
|
||||
|
||||
# passing the owner and group names for tar archiving
|
||||
if cmd_name == 'bdist_dumb':
|
||||
sub_cmd.owner = self.owner
|
||||
sub_cmd.group = self.group
|
||||
|
||||
# If we're going to need to run this command again, tell it to
|
||||
# keep its temporary files around so subsequent runs go faster.
|
||||
if cmd_name in commands[i + 1 :]:
|
||||
sub_cmd.keep_temp = True
|
||||
self.run_command(cmd_name)
|
||||
@@ -0,0 +1,141 @@
|
||||
"""distutils.command.bdist_dumb
|
||||
|
||||
Implements the Distutils 'bdist_dumb' command (create a "dumb" built
|
||||
distribution -- i.e., just an archive to be unpacked under $prefix or
|
||||
$exec_prefix)."""
|
||||
|
||||
import os
|
||||
from distutils._log import log
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..dir_util import ensure_relative, remove_tree
|
||||
from ..errors import DistutilsPlatformError
|
||||
from ..sysconfig import get_python_version
|
||||
from ..util import get_platform
|
||||
|
||||
|
||||
class bdist_dumb(Command):
|
||||
description = "create a \"dumb\" built distribution"
|
||||
|
||||
user_options = [
|
||||
('bdist-dir=', 'd', "temporary directory for creating the distribution"),
|
||||
(
|
||||
'plat-name=',
|
||||
'p',
|
||||
"platform name to embed in generated filenames "
|
||||
f"[default: {get_platform()}]",
|
||||
),
|
||||
(
|
||||
'format=',
|
||||
'f',
|
||||
"archive format to create (tar, gztar, bztar, xztar, ztar, zip)",
|
||||
),
|
||||
(
|
||||
'keep-temp',
|
||||
'k',
|
||||
"keep the pseudo-installation tree around after creating the distribution archive",
|
||||
),
|
||||
('dist-dir=', 'd', "directory to put final built distributions in"),
|
||||
('skip-build', None, "skip rebuilding everything (for testing/debugging)"),
|
||||
(
|
||||
'relative',
|
||||
None,
|
||||
"build the archive using relative paths [default: false]",
|
||||
),
|
||||
(
|
||||
'owner=',
|
||||
'u',
|
||||
"Owner name used when creating a tar file [default: current user]",
|
||||
),
|
||||
(
|
||||
'group=',
|
||||
'g',
|
||||
"Group name used when creating a tar file [default: current group]",
|
||||
),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['keep-temp', 'skip-build', 'relative']
|
||||
|
||||
default_format = {'posix': 'gztar', 'nt': 'zip'}
|
||||
|
||||
def initialize_options(self):
|
||||
self.bdist_dir = None
|
||||
self.plat_name = None
|
||||
self.format = None
|
||||
self.keep_temp = False
|
||||
self.dist_dir = None
|
||||
self.skip_build = None
|
||||
self.relative = False
|
||||
self.owner = None
|
||||
self.group = None
|
||||
|
||||
def finalize_options(self):
|
||||
if self.bdist_dir is None:
|
||||
bdist_base = self.get_finalized_command('bdist').bdist_base
|
||||
self.bdist_dir = os.path.join(bdist_base, 'dumb')
|
||||
|
||||
if self.format is None:
|
||||
try:
|
||||
self.format = self.default_format[os.name]
|
||||
except KeyError:
|
||||
raise DistutilsPlatformError(
|
||||
"don't know how to create dumb built distributions "
|
||||
f"on platform {os.name}"
|
||||
)
|
||||
|
||||
self.set_undefined_options(
|
||||
'bdist',
|
||||
('dist_dir', 'dist_dir'),
|
||||
('plat_name', 'plat_name'),
|
||||
('skip_build', 'skip_build'),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
if not self.skip_build:
|
||||
self.run_command('build')
|
||||
|
||||
install = self.reinitialize_command('install', reinit_subcommands=True)
|
||||
install.root = self.bdist_dir
|
||||
install.skip_build = self.skip_build
|
||||
install.warn_dir = False
|
||||
|
||||
log.info("installing to %s", self.bdist_dir)
|
||||
self.run_command('install')
|
||||
|
||||
# And make an archive relative to the root of the
|
||||
# pseudo-installation tree.
|
||||
archive_basename = f"{self.distribution.get_fullname()}.{self.plat_name}"
|
||||
|
||||
pseudoinstall_root = os.path.join(self.dist_dir, archive_basename)
|
||||
if not self.relative:
|
||||
archive_root = self.bdist_dir
|
||||
else:
|
||||
if self.distribution.has_ext_modules() and (
|
||||
install.install_base != install.install_platbase
|
||||
):
|
||||
raise DistutilsPlatformError(
|
||||
"can't make a dumb built distribution where "
|
||||
f"base and platbase are different ({install.install_base!r}, {install.install_platbase!r})"
|
||||
)
|
||||
else:
|
||||
archive_root = os.path.join(
|
||||
self.bdist_dir, ensure_relative(install.install_base)
|
||||
)
|
||||
|
||||
# Make the archive
|
||||
filename = self.make_archive(
|
||||
pseudoinstall_root,
|
||||
self.format,
|
||||
root_dir=archive_root,
|
||||
owner=self.owner,
|
||||
group=self.group,
|
||||
)
|
||||
if self.distribution.has_ext_modules():
|
||||
pyversion = get_python_version()
|
||||
else:
|
||||
pyversion = 'any'
|
||||
self.distribution.dist_files.append(('bdist_dumb', pyversion, filename))
|
||||
|
||||
if not self.keep_temp:
|
||||
remove_tree(self.bdist_dir, dry_run=self.dry_run)
|
||||
@@ -0,0 +1,598 @@
|
||||
"""distutils.command.bdist_rpm
|
||||
|
||||
Implements the Distutils 'bdist_rpm' command (create RPM source and binary
|
||||
distributions)."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from distutils._log import log
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..debug import DEBUG
|
||||
from ..errors import (
|
||||
DistutilsExecError,
|
||||
DistutilsFileError,
|
||||
DistutilsOptionError,
|
||||
DistutilsPlatformError,
|
||||
)
|
||||
from ..file_util import write_file
|
||||
from ..sysconfig import get_python_version
|
||||
|
||||
|
||||
class bdist_rpm(Command):
|
||||
description = "create an RPM distribution"
|
||||
|
||||
user_options = [
|
||||
('bdist-base=', None, "base directory for creating built distributions"),
|
||||
(
|
||||
'rpm-base=',
|
||||
None,
|
||||
"base directory for creating RPMs (defaults to \"rpm\" under "
|
||||
"--bdist-base; must be specified for RPM 2)",
|
||||
),
|
||||
(
|
||||
'dist-dir=',
|
||||
'd',
|
||||
"directory to put final RPM files in (and .spec files if --spec-only)",
|
||||
),
|
||||
(
|
||||
'python=',
|
||||
None,
|
||||
"path to Python interpreter to hard-code in the .spec file "
|
||||
"[default: \"python\"]",
|
||||
),
|
||||
(
|
||||
'fix-python',
|
||||
None,
|
||||
"hard-code the exact path to the current Python interpreter in "
|
||||
"the .spec file",
|
||||
),
|
||||
('spec-only', None, "only regenerate spec file"),
|
||||
('source-only', None, "only generate source RPM"),
|
||||
('binary-only', None, "only generate binary RPM"),
|
||||
('use-bzip2', None, "use bzip2 instead of gzip to create source distribution"),
|
||||
# More meta-data: too RPM-specific to put in the setup script,
|
||||
# but needs to go in the .spec file -- so we make these options
|
||||
# to "bdist_rpm". The idea is that packagers would put this
|
||||
# info in setup.cfg, although they are of course free to
|
||||
# supply it on the command line.
|
||||
(
|
||||
'distribution-name=',
|
||||
None,
|
||||
"name of the (Linux) distribution to which this "
|
||||
"RPM applies (*not* the name of the module distribution!)",
|
||||
),
|
||||
('group=', None, "package classification [default: \"Development/Libraries\"]"),
|
||||
('release=', None, "RPM release number"),
|
||||
('serial=', None, "RPM serial number"),
|
||||
(
|
||||
'vendor=',
|
||||
None,
|
||||
"RPM \"vendor\" (eg. \"Joe Blow <joe@example.com>\") "
|
||||
"[default: maintainer or author from setup script]",
|
||||
),
|
||||
(
|
||||
'packager=',
|
||||
None,
|
||||
"RPM packager (eg. \"Jane Doe <jane@example.net>\") [default: vendor]",
|
||||
),
|
||||
('doc-files=', None, "list of documentation files (space or comma-separated)"),
|
||||
('changelog=', None, "RPM changelog"),
|
||||
('icon=', None, "name of icon file"),
|
||||
('provides=', None, "capabilities provided by this package"),
|
||||
('requires=', None, "capabilities required by this package"),
|
||||
('conflicts=', None, "capabilities which conflict with this package"),
|
||||
('build-requires=', None, "capabilities required to build this package"),
|
||||
('obsoletes=', None, "capabilities made obsolete by this package"),
|
||||
('no-autoreq', None, "do not automatically calculate dependencies"),
|
||||
# Actions to take when building RPM
|
||||
('keep-temp', 'k', "don't clean up RPM build directory"),
|
||||
('no-keep-temp', None, "clean up RPM build directory [default]"),
|
||||
(
|
||||
'use-rpm-opt-flags',
|
||||
None,
|
||||
"compile with RPM_OPT_FLAGS when building from source RPM",
|
||||
),
|
||||
('no-rpm-opt-flags', None, "do not pass any RPM CFLAGS to compiler"),
|
||||
('rpm3-mode', None, "RPM 3 compatibility mode (default)"),
|
||||
('rpm2-mode', None, "RPM 2 compatibility mode"),
|
||||
# Add the hooks necessary for specifying custom scripts
|
||||
('prep-script=', None, "Specify a script for the PREP phase of RPM building"),
|
||||
('build-script=', None, "Specify a script for the BUILD phase of RPM building"),
|
||||
(
|
||||
'pre-install=',
|
||||
None,
|
||||
"Specify a script for the pre-INSTALL phase of RPM building",
|
||||
),
|
||||
(
|
||||
'install-script=',
|
||||
None,
|
||||
"Specify a script for the INSTALL phase of RPM building",
|
||||
),
|
||||
(
|
||||
'post-install=',
|
||||
None,
|
||||
"Specify a script for the post-INSTALL phase of RPM building",
|
||||
),
|
||||
(
|
||||
'pre-uninstall=',
|
||||
None,
|
||||
"Specify a script for the pre-UNINSTALL phase of RPM building",
|
||||
),
|
||||
(
|
||||
'post-uninstall=',
|
||||
None,
|
||||
"Specify a script for the post-UNINSTALL phase of RPM building",
|
||||
),
|
||||
('clean-script=', None, "Specify a script for the CLEAN phase of RPM building"),
|
||||
(
|
||||
'verify-script=',
|
||||
None,
|
||||
"Specify a script for the VERIFY phase of the RPM build",
|
||||
),
|
||||
# Allow a packager to explicitly force an architecture
|
||||
('force-arch=', None, "Force an architecture onto the RPM build process"),
|
||||
('quiet', 'q', "Run the INSTALL phase of RPM building in quiet mode"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = [
|
||||
'keep-temp',
|
||||
'use-rpm-opt-flags',
|
||||
'rpm3-mode',
|
||||
'no-autoreq',
|
||||
'quiet',
|
||||
]
|
||||
|
||||
negative_opt: ClassVar[dict[str, str]] = {
|
||||
'no-keep-temp': 'keep-temp',
|
||||
'no-rpm-opt-flags': 'use-rpm-opt-flags',
|
||||
'rpm2-mode': 'rpm3-mode',
|
||||
}
|
||||
|
||||
def initialize_options(self):
|
||||
self.bdist_base = None
|
||||
self.rpm_base = None
|
||||
self.dist_dir = None
|
||||
self.python = None
|
||||
self.fix_python = None
|
||||
self.spec_only = None
|
||||
self.binary_only = None
|
||||
self.source_only = None
|
||||
self.use_bzip2 = None
|
||||
|
||||
self.distribution_name = None
|
||||
self.group = None
|
||||
self.release = None
|
||||
self.serial = None
|
||||
self.vendor = None
|
||||
self.packager = None
|
||||
self.doc_files = None
|
||||
self.changelog = None
|
||||
self.icon = None
|
||||
|
||||
self.prep_script = None
|
||||
self.build_script = None
|
||||
self.install_script = None
|
||||
self.clean_script = None
|
||||
self.verify_script = None
|
||||
self.pre_install = None
|
||||
self.post_install = None
|
||||
self.pre_uninstall = None
|
||||
self.post_uninstall = None
|
||||
self.prep = None
|
||||
self.provides = None
|
||||
self.requires = None
|
||||
self.conflicts = None
|
||||
self.build_requires = None
|
||||
self.obsoletes = None
|
||||
|
||||
self.keep_temp = False
|
||||
self.use_rpm_opt_flags = True
|
||||
self.rpm3_mode = True
|
||||
self.no_autoreq = False
|
||||
|
||||
self.force_arch = None
|
||||
self.quiet = False
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
self.set_undefined_options('bdist', ('bdist_base', 'bdist_base'))
|
||||
if self.rpm_base is None:
|
||||
if not self.rpm3_mode:
|
||||
raise DistutilsOptionError("you must specify --rpm-base in RPM 2 mode")
|
||||
self.rpm_base = os.path.join(self.bdist_base, "rpm")
|
||||
|
||||
if self.python is None:
|
||||
if self.fix_python:
|
||||
self.python = sys.executable
|
||||
else:
|
||||
self.python = "python3"
|
||||
elif self.fix_python:
|
||||
raise DistutilsOptionError(
|
||||
"--python and --fix-python are mutually exclusive options"
|
||||
)
|
||||
|
||||
if os.name != 'posix':
|
||||
raise DistutilsPlatformError(
|
||||
f"don't know how to create RPM distributions on platform {os.name}"
|
||||
)
|
||||
if self.binary_only and self.source_only:
|
||||
raise DistutilsOptionError(
|
||||
"cannot supply both '--source-only' and '--binary-only'"
|
||||
)
|
||||
|
||||
# don't pass CFLAGS to pure python distributions
|
||||
if not self.distribution.has_ext_modules():
|
||||
self.use_rpm_opt_flags = False
|
||||
|
||||
self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
|
||||
self.finalize_package_data()
|
||||
|
||||
def finalize_package_data(self) -> None:
|
||||
self.ensure_string('group', "Development/Libraries")
|
||||
self.ensure_string(
|
||||
'vendor',
|
||||
f"{self.distribution.get_contact()} <{self.distribution.get_contact_email()}>",
|
||||
)
|
||||
self.ensure_string('packager')
|
||||
self.ensure_string_list('doc_files')
|
||||
if isinstance(self.doc_files, list):
|
||||
for readme in ('README', 'README.txt'):
|
||||
if os.path.exists(readme) and readme not in self.doc_files:
|
||||
self.doc_files.append(readme)
|
||||
|
||||
self.ensure_string('release', "1")
|
||||
self.ensure_string('serial') # should it be an int?
|
||||
|
||||
self.ensure_string('distribution_name')
|
||||
|
||||
self.ensure_string('changelog')
|
||||
# Format changelog correctly
|
||||
self.changelog = self._format_changelog(self.changelog)
|
||||
|
||||
self.ensure_filename('icon')
|
||||
|
||||
self.ensure_filename('prep_script')
|
||||
self.ensure_filename('build_script')
|
||||
self.ensure_filename('install_script')
|
||||
self.ensure_filename('clean_script')
|
||||
self.ensure_filename('verify_script')
|
||||
self.ensure_filename('pre_install')
|
||||
self.ensure_filename('post_install')
|
||||
self.ensure_filename('pre_uninstall')
|
||||
self.ensure_filename('post_uninstall')
|
||||
|
||||
# XXX don't forget we punted on summaries and descriptions -- they
|
||||
# should be handled here eventually!
|
||||
|
||||
# Now *this* is some meta-data that belongs in the setup script...
|
||||
self.ensure_string_list('provides')
|
||||
self.ensure_string_list('requires')
|
||||
self.ensure_string_list('conflicts')
|
||||
self.ensure_string_list('build_requires')
|
||||
self.ensure_string_list('obsoletes')
|
||||
|
||||
self.ensure_string('force_arch')
|
||||
|
||||
def run(self) -> None: # noqa: C901
|
||||
if DEBUG:
|
||||
print("before _get_package_data():")
|
||||
print("vendor =", self.vendor)
|
||||
print("packager =", self.packager)
|
||||
print("doc_files =", self.doc_files)
|
||||
print("changelog =", self.changelog)
|
||||
|
||||
# make directories
|
||||
if self.spec_only:
|
||||
spec_dir = self.dist_dir
|
||||
self.mkpath(spec_dir)
|
||||
else:
|
||||
rpm_dir = {}
|
||||
for d in ('SOURCES', 'SPECS', 'BUILD', 'RPMS', 'SRPMS'):
|
||||
rpm_dir[d] = os.path.join(self.rpm_base, d)
|
||||
self.mkpath(rpm_dir[d])
|
||||
spec_dir = rpm_dir['SPECS']
|
||||
|
||||
# Spec file goes into 'dist_dir' if '--spec-only specified',
|
||||
# build/rpm.<plat> otherwise.
|
||||
spec_path = os.path.join(spec_dir, f"{self.distribution.get_name()}.spec")
|
||||
self.execute(
|
||||
write_file, (spec_path, self._make_spec_file()), f"writing '{spec_path}'"
|
||||
)
|
||||
|
||||
if self.spec_only: # stop if requested
|
||||
return
|
||||
|
||||
# Make a source distribution and copy to SOURCES directory with
|
||||
# optional icon.
|
||||
saved_dist_files = self.distribution.dist_files[:]
|
||||
sdist = self.reinitialize_command('sdist')
|
||||
if self.use_bzip2:
|
||||
sdist.formats = ['bztar']
|
||||
else:
|
||||
sdist.formats = ['gztar']
|
||||
self.run_command('sdist')
|
||||
self.distribution.dist_files = saved_dist_files
|
||||
|
||||
source = sdist.get_archive_files()[0]
|
||||
source_dir = rpm_dir['SOURCES']
|
||||
self.copy_file(source, source_dir)
|
||||
|
||||
if self.icon:
|
||||
if os.path.exists(self.icon):
|
||||
self.copy_file(self.icon, source_dir)
|
||||
else:
|
||||
raise DistutilsFileError(f"icon file '{self.icon}' does not exist")
|
||||
|
||||
# build package
|
||||
log.info("building RPMs")
|
||||
rpm_cmd = ['rpmbuild']
|
||||
|
||||
if self.source_only: # what kind of RPMs?
|
||||
rpm_cmd.append('-bs')
|
||||
elif self.binary_only:
|
||||
rpm_cmd.append('-bb')
|
||||
else:
|
||||
rpm_cmd.append('-ba')
|
||||
rpm_cmd.extend(['--define', f'__python {self.python}'])
|
||||
if self.rpm3_mode:
|
||||
rpm_cmd.extend(['--define', f'_topdir {os.path.abspath(self.rpm_base)}'])
|
||||
if not self.keep_temp:
|
||||
rpm_cmd.append('--clean')
|
||||
|
||||
if self.quiet:
|
||||
rpm_cmd.append('--quiet')
|
||||
|
||||
rpm_cmd.append(spec_path)
|
||||
# Determine the binary rpm names that should be built out of this spec
|
||||
# file
|
||||
# Note that some of these may not be really built (if the file
|
||||
# list is empty)
|
||||
nvr_string = "%{name}-%{version}-%{release}"
|
||||
src_rpm = nvr_string + ".src.rpm"
|
||||
non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm"
|
||||
q_cmd = rf"rpm -q --qf '{src_rpm} {non_src_rpm}\n' --specfile '{spec_path}'"
|
||||
|
||||
out = os.popen(q_cmd)
|
||||
try:
|
||||
binary_rpms = []
|
||||
source_rpm = None
|
||||
while True:
|
||||
line = out.readline()
|
||||
if not line:
|
||||
break
|
||||
ell = line.strip().split()
|
||||
assert len(ell) == 2
|
||||
binary_rpms.append(ell[1])
|
||||
# The source rpm is named after the first entry in the spec file
|
||||
if source_rpm is None:
|
||||
source_rpm = ell[0]
|
||||
|
||||
status = out.close()
|
||||
if status:
|
||||
raise DistutilsExecError(f"Failed to execute: {q_cmd!r}")
|
||||
|
||||
finally:
|
||||
out.close()
|
||||
|
||||
self.spawn(rpm_cmd)
|
||||
|
||||
if not self.dry_run:
|
||||
if self.distribution.has_ext_modules():
|
||||
pyversion = get_python_version()
|
||||
else:
|
||||
pyversion = 'any'
|
||||
|
||||
if not self.binary_only:
|
||||
srpm = os.path.join(rpm_dir['SRPMS'], source_rpm)
|
||||
assert os.path.exists(srpm)
|
||||
self.move_file(srpm, self.dist_dir)
|
||||
filename = os.path.join(self.dist_dir, source_rpm)
|
||||
self.distribution.dist_files.append(('bdist_rpm', pyversion, filename))
|
||||
|
||||
if not self.source_only:
|
||||
for rpm in binary_rpms:
|
||||
rpm = os.path.join(rpm_dir['RPMS'], rpm)
|
||||
if os.path.exists(rpm):
|
||||
self.move_file(rpm, self.dist_dir)
|
||||
filename = os.path.join(self.dist_dir, os.path.basename(rpm))
|
||||
self.distribution.dist_files.append((
|
||||
'bdist_rpm',
|
||||
pyversion,
|
||||
filename,
|
||||
))
|
||||
|
||||
def _dist_path(self, path):
|
||||
return os.path.join(self.dist_dir, os.path.basename(path))
|
||||
|
||||
def _make_spec_file(self): # noqa: C901
|
||||
"""Generate the text of an RPM spec file and return it as a
|
||||
list of strings (one per line).
|
||||
"""
|
||||
# definitions and headers
|
||||
spec_file = [
|
||||
'%define name ' + self.distribution.get_name(),
|
||||
'%define version ' + self.distribution.get_version().replace('-', '_'),
|
||||
'%define unmangled_version ' + self.distribution.get_version(),
|
||||
'%define release ' + self.release.replace('-', '_'),
|
||||
'',
|
||||
'Summary: ' + (self.distribution.get_description() or "UNKNOWN"),
|
||||
]
|
||||
|
||||
# Workaround for #14443 which affects some RPM based systems such as
|
||||
# RHEL6 (and probably derivatives)
|
||||
vendor_hook = subprocess.getoutput('rpm --eval %{__os_install_post}')
|
||||
# Generate a potential replacement value for __os_install_post (whilst
|
||||
# normalizing the whitespace to simplify the test for whether the
|
||||
# invocation of brp-python-bytecompile passes in __python):
|
||||
vendor_hook = '\n'.join([
|
||||
f' {line.strip()} \\' for line in vendor_hook.splitlines()
|
||||
])
|
||||
problem = "brp-python-bytecompile \\\n"
|
||||
fixed = "brp-python-bytecompile %{__python} \\\n"
|
||||
fixed_hook = vendor_hook.replace(problem, fixed)
|
||||
if fixed_hook != vendor_hook:
|
||||
spec_file.append('# Workaround for https://bugs.python.org/issue14443')
|
||||
spec_file.append('%define __os_install_post ' + fixed_hook + '\n')
|
||||
|
||||
# put locale summaries into spec file
|
||||
# XXX not supported for now (hard to put a dictionary
|
||||
# in a config file -- arg!)
|
||||
# for locale in self.summaries.keys():
|
||||
# spec_file.append('Summary(%s): %s' % (locale,
|
||||
# self.summaries[locale]))
|
||||
|
||||
spec_file.extend([
|
||||
'Name: %{name}',
|
||||
'Version: %{version}',
|
||||
'Release: %{release}',
|
||||
])
|
||||
|
||||
# XXX yuck! this filename is available from the "sdist" command,
|
||||
# but only after it has run: and we create the spec file before
|
||||
# running "sdist", in case of --spec-only.
|
||||
if self.use_bzip2:
|
||||
spec_file.append('Source0: %{name}-%{unmangled_version}.tar.bz2')
|
||||
else:
|
||||
spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz')
|
||||
|
||||
spec_file.extend([
|
||||
'License: ' + (self.distribution.get_license() or "UNKNOWN"),
|
||||
'Group: ' + self.group,
|
||||
'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot',
|
||||
'Prefix: %{_prefix}',
|
||||
])
|
||||
|
||||
if not self.force_arch:
|
||||
# noarch if no extension modules
|
||||
if not self.distribution.has_ext_modules():
|
||||
spec_file.append('BuildArch: noarch')
|
||||
else:
|
||||
spec_file.append(f'BuildArch: {self.force_arch}')
|
||||
|
||||
for field in (
|
||||
'Vendor',
|
||||
'Packager',
|
||||
'Provides',
|
||||
'Requires',
|
||||
'Conflicts',
|
||||
'Obsoletes',
|
||||
):
|
||||
val = getattr(self, field.lower())
|
||||
if isinstance(val, list):
|
||||
spec_file.append('{}: {}'.format(field, ' '.join(val)))
|
||||
elif val is not None:
|
||||
spec_file.append(f'{field}: {val}')
|
||||
|
||||
if self.distribution.get_url():
|
||||
spec_file.append('Url: ' + self.distribution.get_url())
|
||||
|
||||
if self.distribution_name:
|
||||
spec_file.append('Distribution: ' + self.distribution_name)
|
||||
|
||||
if self.build_requires:
|
||||
spec_file.append('BuildRequires: ' + ' '.join(self.build_requires))
|
||||
|
||||
if self.icon:
|
||||
spec_file.append('Icon: ' + os.path.basename(self.icon))
|
||||
|
||||
if self.no_autoreq:
|
||||
spec_file.append('AutoReq: 0')
|
||||
|
||||
spec_file.extend([
|
||||
'',
|
||||
'%description',
|
||||
self.distribution.get_long_description() or "",
|
||||
])
|
||||
|
||||
# put locale descriptions into spec file
|
||||
# XXX again, suppressed because config file syntax doesn't
|
||||
# easily support this ;-(
|
||||
# for locale in self.descriptions.keys():
|
||||
# spec_file.extend([
|
||||
# '',
|
||||
# '%description -l ' + locale,
|
||||
# self.descriptions[locale],
|
||||
# ])
|
||||
|
||||
# rpm scripts
|
||||
# figure out default build script
|
||||
def_setup_call = f"{self.python} {os.path.basename(sys.argv[0])}"
|
||||
def_build = f"{def_setup_call} build"
|
||||
if self.use_rpm_opt_flags:
|
||||
def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build
|
||||
|
||||
# insert contents of files
|
||||
|
||||
# XXX this is kind of misleading: user-supplied options are files
|
||||
# that we open and interpolate into the spec file, but the defaults
|
||||
# are just text that we drop in as-is. Hmmm.
|
||||
|
||||
install_cmd = f'{def_setup_call} install -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES'
|
||||
|
||||
script_options = [
|
||||
('prep', 'prep_script', "%setup -n %{name}-%{unmangled_version}"),
|
||||
('build', 'build_script', def_build),
|
||||
('install', 'install_script', install_cmd),
|
||||
('clean', 'clean_script', "rm -rf $RPM_BUILD_ROOT"),
|
||||
('verifyscript', 'verify_script', None),
|
||||
('pre', 'pre_install', None),
|
||||
('post', 'post_install', None),
|
||||
('preun', 'pre_uninstall', None),
|
||||
('postun', 'post_uninstall', None),
|
||||
]
|
||||
|
||||
for rpm_opt, attr, default in script_options:
|
||||
# Insert contents of file referred to, if no file is referred to
|
||||
# use 'default' as contents of script
|
||||
val = getattr(self, attr)
|
||||
if val or default:
|
||||
spec_file.extend([
|
||||
'',
|
||||
'%' + rpm_opt,
|
||||
])
|
||||
if val:
|
||||
with open(val) as f:
|
||||
spec_file.extend(f.read().split('\n'))
|
||||
else:
|
||||
spec_file.append(default)
|
||||
|
||||
# files section
|
||||
spec_file.extend([
|
||||
'',
|
||||
'%files -f INSTALLED_FILES',
|
||||
'%defattr(-,root,root)',
|
||||
])
|
||||
|
||||
if self.doc_files:
|
||||
spec_file.append('%doc ' + ' '.join(self.doc_files))
|
||||
|
||||
if self.changelog:
|
||||
spec_file.extend([
|
||||
'',
|
||||
'%changelog',
|
||||
])
|
||||
spec_file.extend(self.changelog)
|
||||
|
||||
return spec_file
|
||||
|
||||
def _format_changelog(self, changelog):
|
||||
"""Format the changelog correctly and convert it to a list of strings"""
|
||||
if not changelog:
|
||||
return changelog
|
||||
new_changelog = []
|
||||
for line in changelog.strip().split('\n'):
|
||||
line = line.strip()
|
||||
if line[0] == '*':
|
||||
new_changelog.extend(['', line])
|
||||
elif line[0] == '-':
|
||||
new_changelog.append(line)
|
||||
else:
|
||||
new_changelog.append(' ' + line)
|
||||
|
||||
# strip trailing newline inserted by first changelog entry
|
||||
if not new_changelog[0]:
|
||||
del new_changelog[0]
|
||||
|
||||
return new_changelog
|
||||
156
.venv/Lib/site-packages/setuptools/_distutils/command/build.py
Normal file
156
.venv/Lib/site-packages/setuptools/_distutils/command/build.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""distutils.command.build
|
||||
|
||||
Implements the Distutils 'build' command."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
from collections.abc import Callable
|
||||
from typing import ClassVar
|
||||
|
||||
from ..ccompiler import show_compilers
|
||||
from ..core import Command
|
||||
from ..errors import DistutilsOptionError
|
||||
from ..util import get_platform
|
||||
|
||||
|
||||
class build(Command):
|
||||
description = "build everything needed to install"
|
||||
|
||||
user_options = [
|
||||
('build-base=', 'b', "base directory for build library"),
|
||||
('build-purelib=', None, "build directory for platform-neutral distributions"),
|
||||
('build-platlib=', None, "build directory for platform-specific distributions"),
|
||||
(
|
||||
'build-lib=',
|
||||
None,
|
||||
"build directory for all distribution (defaults to either build-purelib or build-platlib",
|
||||
),
|
||||
('build-scripts=', None, "build directory for scripts"),
|
||||
('build-temp=', 't', "temporary build directory"),
|
||||
(
|
||||
'plat-name=',
|
||||
'p',
|
||||
f"platform name to build for, if supported [default: {get_platform()}]",
|
||||
),
|
||||
('compiler=', 'c', "specify the compiler type"),
|
||||
('parallel=', 'j', "number of parallel build jobs"),
|
||||
('debug', 'g', "compile extensions and libraries with debugging information"),
|
||||
('force', 'f', "forcibly build everything (ignore file timestamps)"),
|
||||
('executable=', 'e', "specify final destination interpreter path (build.py)"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['debug', 'force']
|
||||
|
||||
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
|
||||
('help-compiler', None, "list available compilers", show_compilers),
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
self.build_base = 'build'
|
||||
# these are decided only after 'build_base' has its final value
|
||||
# (unless overridden by the user or client)
|
||||
self.build_purelib = None
|
||||
self.build_platlib = None
|
||||
self.build_lib = None
|
||||
self.build_temp = None
|
||||
self.build_scripts = None
|
||||
self.compiler = None
|
||||
self.plat_name = None
|
||||
self.debug = None
|
||||
self.force = False
|
||||
self.executable = None
|
||||
self.parallel = None
|
||||
|
||||
def finalize_options(self) -> None: # noqa: C901
|
||||
if self.plat_name is None:
|
||||
self.plat_name = get_platform()
|
||||
else:
|
||||
# plat-name only supported for windows (other platforms are
|
||||
# supported via ./configure flags, if at all). Avoid misleading
|
||||
# other platforms.
|
||||
if os.name != 'nt':
|
||||
raise DistutilsOptionError(
|
||||
"--plat-name only supported on Windows (try "
|
||||
"using './configure --help' on your platform)"
|
||||
)
|
||||
|
||||
plat_specifier = f".{self.plat_name}-{sys.implementation.cache_tag}"
|
||||
|
||||
# Python 3.13+ with --disable-gil shouldn't share build directories
|
||||
if sysconfig.get_config_var('Py_GIL_DISABLED'):
|
||||
plat_specifier += 't'
|
||||
|
||||
# Make it so Python 2.x and Python 2.x with --with-pydebug don't
|
||||
# share the same build directories. Doing so confuses the build
|
||||
# process for C modules
|
||||
if hasattr(sys, 'gettotalrefcount'):
|
||||
plat_specifier += '-pydebug'
|
||||
|
||||
# 'build_purelib' and 'build_platlib' just default to 'lib' and
|
||||
# 'lib.<plat>' under the base build directory. We only use one of
|
||||
# them for a given distribution, though --
|
||||
if self.build_purelib is None:
|
||||
self.build_purelib = os.path.join(self.build_base, 'lib')
|
||||
if self.build_platlib is None:
|
||||
self.build_platlib = os.path.join(self.build_base, 'lib' + plat_specifier)
|
||||
|
||||
# 'build_lib' is the actual directory that we will use for this
|
||||
# particular module distribution -- if user didn't supply it, pick
|
||||
# one of 'build_purelib' or 'build_platlib'.
|
||||
if self.build_lib is None:
|
||||
if self.distribution.has_ext_modules():
|
||||
self.build_lib = self.build_platlib
|
||||
else:
|
||||
self.build_lib = self.build_purelib
|
||||
|
||||
# 'build_temp' -- temporary directory for compiler turds,
|
||||
# "build/temp.<plat>"
|
||||
if self.build_temp is None:
|
||||
self.build_temp = os.path.join(self.build_base, 'temp' + plat_specifier)
|
||||
if self.build_scripts is None:
|
||||
self.build_scripts = os.path.join(
|
||||
self.build_base,
|
||||
f'scripts-{sys.version_info.major}.{sys.version_info.minor}',
|
||||
)
|
||||
|
||||
if self.executable is None and sys.executable:
|
||||
self.executable = os.path.normpath(sys.executable)
|
||||
|
||||
if isinstance(self.parallel, str):
|
||||
try:
|
||||
self.parallel = int(self.parallel)
|
||||
except ValueError:
|
||||
raise DistutilsOptionError("parallel should be an integer")
|
||||
|
||||
def run(self) -> None:
|
||||
# Run all relevant sub-commands. This will be some subset of:
|
||||
# - build_py - pure Python modules
|
||||
# - build_clib - standalone C libraries
|
||||
# - build_ext - Python extensions
|
||||
# - build_scripts - (Python) scripts
|
||||
for cmd_name in self.get_sub_commands():
|
||||
self.run_command(cmd_name)
|
||||
|
||||
# -- Predicates for the sub-command list ---------------------------
|
||||
|
||||
def has_pure_modules(self):
|
||||
return self.distribution.has_pure_modules()
|
||||
|
||||
def has_c_libraries(self):
|
||||
return self.distribution.has_c_libraries()
|
||||
|
||||
def has_ext_modules(self):
|
||||
return self.distribution.has_ext_modules()
|
||||
|
||||
def has_scripts(self):
|
||||
return self.distribution.has_scripts()
|
||||
|
||||
sub_commands = [
|
||||
('build_py', has_pure_modules),
|
||||
('build_clib', has_c_libraries),
|
||||
('build_ext', has_ext_modules),
|
||||
('build_scripts', has_scripts),
|
||||
]
|
||||
@@ -0,0 +1,201 @@
|
||||
"""distutils.command.build_clib
|
||||
|
||||
Implements the Distutils 'build_clib' command, to build a C/C++ library
|
||||
that is included in the module distribution and needed by an extension
|
||||
module."""
|
||||
|
||||
# XXX this module has *lots* of code ripped-off quite transparently from
|
||||
# build_ext.py -- not surprisingly really, as the work required to build
|
||||
# a static library from a collection of C source files is not really all
|
||||
# that different from what's required to build a shared object file from
|
||||
# a collection of C source files. Nevertheless, I haven't done the
|
||||
# necessary refactoring to account for the overlap in code between the
|
||||
# two modules, mainly because a number of subtle details changed in the
|
||||
# cut 'n paste. Sigh.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from distutils._log import log
|
||||
from typing import ClassVar
|
||||
|
||||
from ..ccompiler import new_compiler, show_compilers
|
||||
from ..core import Command
|
||||
from ..errors import DistutilsSetupError
|
||||
from ..sysconfig import customize_compiler
|
||||
|
||||
|
||||
class build_clib(Command):
|
||||
description = "build C/C++ libraries used by Python extensions"
|
||||
|
||||
user_options: ClassVar[list[tuple[str, str, str]]] = [
|
||||
('build-clib=', 'b', "directory to build C/C++ libraries to"),
|
||||
('build-temp=', 't', "directory to put temporary build by-products"),
|
||||
('debug', 'g', "compile with debugging information"),
|
||||
('force', 'f', "forcibly build everything (ignore file timestamps)"),
|
||||
('compiler=', 'c', "specify the compiler type"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['debug', 'force']
|
||||
|
||||
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
|
||||
('help-compiler', None, "list available compilers", show_compilers),
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
self.build_clib = None
|
||||
self.build_temp = None
|
||||
|
||||
# List of libraries to build
|
||||
self.libraries = None
|
||||
|
||||
# Compilation options for all libraries
|
||||
self.include_dirs = None
|
||||
self.define = None
|
||||
self.undef = None
|
||||
self.debug = None
|
||||
self.force = False
|
||||
self.compiler = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
# This might be confusing: both build-clib and build-temp default
|
||||
# to build-temp as defined by the "build" command. This is because
|
||||
# I think that C libraries are really just temporary build
|
||||
# by-products, at least from the point of view of building Python
|
||||
# extensions -- but I want to keep my options open.
|
||||
self.set_undefined_options(
|
||||
'build',
|
||||
('build_temp', 'build_clib'),
|
||||
('build_temp', 'build_temp'),
|
||||
('compiler', 'compiler'),
|
||||
('debug', 'debug'),
|
||||
('force', 'force'),
|
||||
)
|
||||
|
||||
self.libraries = self.distribution.libraries
|
||||
if self.libraries:
|
||||
self.check_library_list(self.libraries)
|
||||
|
||||
if self.include_dirs is None:
|
||||
self.include_dirs = self.distribution.include_dirs or []
|
||||
if isinstance(self.include_dirs, str):
|
||||
self.include_dirs = self.include_dirs.split(os.pathsep)
|
||||
|
||||
# XXX same as for build_ext -- what about 'self.define' and
|
||||
# 'self.undef' ?
|
||||
|
||||
def run(self) -> None:
|
||||
if not self.libraries:
|
||||
return
|
||||
|
||||
self.compiler = new_compiler(
|
||||
compiler=self.compiler, dry_run=self.dry_run, force=self.force
|
||||
)
|
||||
customize_compiler(self.compiler)
|
||||
|
||||
if self.include_dirs is not None:
|
||||
self.compiler.set_include_dirs(self.include_dirs)
|
||||
if self.define is not None:
|
||||
# 'define' option is a list of (name,value) tuples
|
||||
for name, value in self.define:
|
||||
self.compiler.define_macro(name, value)
|
||||
if self.undef is not None:
|
||||
for macro in self.undef:
|
||||
self.compiler.undefine_macro(macro)
|
||||
|
||||
self.build_libraries(self.libraries)
|
||||
|
||||
def check_library_list(self, libraries) -> None:
|
||||
"""Ensure that the list of libraries is valid.
|
||||
|
||||
`library` is presumably provided as a command option 'libraries'.
|
||||
This method checks that it is a list of 2-tuples, where the tuples
|
||||
are (library_name, build_info_dict).
|
||||
|
||||
Raise DistutilsSetupError if the structure is invalid anywhere;
|
||||
just returns otherwise.
|
||||
"""
|
||||
if not isinstance(libraries, list):
|
||||
raise DistutilsSetupError("'libraries' option must be a list of tuples")
|
||||
|
||||
for lib in libraries:
|
||||
if not isinstance(lib, tuple) and len(lib) != 2:
|
||||
raise DistutilsSetupError("each element of 'libraries' must a 2-tuple")
|
||||
|
||||
name, build_info = lib
|
||||
|
||||
if not isinstance(name, str):
|
||||
raise DistutilsSetupError(
|
||||
"first element of each tuple in 'libraries' "
|
||||
"must be a string (the library name)"
|
||||
)
|
||||
|
||||
if '/' in name or (os.sep != '/' and os.sep in name):
|
||||
raise DistutilsSetupError(
|
||||
f"bad library name '{lib[0]}': may not contain directory separators"
|
||||
)
|
||||
|
||||
if not isinstance(build_info, dict):
|
||||
raise DistutilsSetupError(
|
||||
"second element of each tuple in 'libraries' "
|
||||
"must be a dictionary (build info)"
|
||||
)
|
||||
|
||||
def get_library_names(self):
|
||||
# Assume the library list is valid -- 'check_library_list()' is
|
||||
# called from 'finalize_options()', so it should be!
|
||||
if not self.libraries:
|
||||
return None
|
||||
|
||||
lib_names = []
|
||||
for lib_name, _build_info in self.libraries:
|
||||
lib_names.append(lib_name)
|
||||
return lib_names
|
||||
|
||||
def get_source_files(self):
|
||||
self.check_library_list(self.libraries)
|
||||
filenames = []
|
||||
for lib_name, build_info in self.libraries:
|
||||
sources = build_info.get('sources')
|
||||
if sources is None or not isinstance(sources, (list, tuple)):
|
||||
raise DistutilsSetupError(
|
||||
f"in 'libraries' option (library '{lib_name}'), "
|
||||
"'sources' must be present and must be "
|
||||
"a list of source filenames"
|
||||
)
|
||||
|
||||
filenames.extend(sources)
|
||||
return filenames
|
||||
|
||||
def build_libraries(self, libraries) -> None:
|
||||
for lib_name, build_info in libraries:
|
||||
sources = build_info.get('sources')
|
||||
if sources is None or not isinstance(sources, (list, tuple)):
|
||||
raise DistutilsSetupError(
|
||||
f"in 'libraries' option (library '{lib_name}'), "
|
||||
"'sources' must be present and must be "
|
||||
"a list of source filenames"
|
||||
)
|
||||
sources = list(sources)
|
||||
|
||||
log.info("building '%s' library", lib_name)
|
||||
|
||||
# First, compile the source code to object files in the library
|
||||
# directory. (This should probably change to putting object
|
||||
# files in a temporary build directory.)
|
||||
macros = build_info.get('macros')
|
||||
include_dirs = build_info.get('include_dirs')
|
||||
objects = self.compiler.compile(
|
||||
sources,
|
||||
output_dir=self.build_temp,
|
||||
macros=macros,
|
||||
include_dirs=include_dirs,
|
||||
debug=self.debug,
|
||||
)
|
||||
|
||||
# Now "link" the object files together into a static library.
|
||||
# (On Unix at least, this isn't really linking -- it just
|
||||
# builds an archive. Whatever.)
|
||||
self.compiler.create_static_lib(
|
||||
objects, lib_name, output_dir=self.build_clib, debug=self.debug
|
||||
)
|
||||
@@ -0,0 +1,812 @@
|
||||
"""distutils.command.build_ext
|
||||
|
||||
Implements the Distutils 'build_ext' command, for building extension
|
||||
modules (currently limited to C extensions, should accommodate C++
|
||||
extensions ASAP)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from distutils._log import log
|
||||
from site import USER_BASE
|
||||
from typing import ClassVar
|
||||
|
||||
from .._modified import newer_group
|
||||
from ..ccompiler import new_compiler, show_compilers
|
||||
from ..core import Command
|
||||
from ..errors import (
|
||||
CCompilerError,
|
||||
CompileError,
|
||||
DistutilsError,
|
||||
DistutilsOptionError,
|
||||
DistutilsPlatformError,
|
||||
DistutilsSetupError,
|
||||
)
|
||||
from ..extension import Extension
|
||||
from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version
|
||||
from ..util import get_platform, is_freethreaded, is_mingw
|
||||
|
||||
# An extension name is just a dot-separated list of Python NAMEs (ie.
|
||||
# the same as a fully-qualified module name).
|
||||
extension_name_re = re.compile(r'^[a-zA-Z_][a-zA-Z_0-9]*(\.[a-zA-Z_][a-zA-Z_0-9]*)*$')
|
||||
|
||||
|
||||
class build_ext(Command):
|
||||
description = "build C/C++ extensions (compile/link to build directory)"
|
||||
|
||||
# XXX thoughts on how to deal with complex command-line options like
|
||||
# these, i.e. how to make it so fancy_getopt can suck them off the
|
||||
# command line and make it look like setup.py defined the appropriate
|
||||
# lists of tuples of what-have-you.
|
||||
# - each command needs a callback to process its command-line options
|
||||
# - Command.__init__() needs access to its share of the whole
|
||||
# command line (must ultimately come from
|
||||
# Distribution.parse_command_line())
|
||||
# - it then calls the current command class' option-parsing
|
||||
# callback to deal with weird options like -D, which have to
|
||||
# parse the option text and churn out some custom data
|
||||
# structure
|
||||
# - that data structure (in this case, a list of 2-tuples)
|
||||
# will then be present in the command object by the time
|
||||
# we get to finalize_options() (i.e. the constructor
|
||||
# takes care of both command-line and client options
|
||||
# in between initialize_options() and finalize_options())
|
||||
|
||||
sep_by = f" (separated by '{os.pathsep}')"
|
||||
user_options = [
|
||||
('build-lib=', 'b', "directory for compiled extension modules"),
|
||||
('build-temp=', 't', "directory for temporary files (build by-products)"),
|
||||
(
|
||||
'plat-name=',
|
||||
'p',
|
||||
"platform name to cross-compile for, if supported "
|
||||
f"[default: {get_platform()}]",
|
||||
),
|
||||
(
|
||||
'inplace',
|
||||
'i',
|
||||
"ignore build-lib and put compiled extensions into the source "
|
||||
"directory alongside your pure Python modules",
|
||||
),
|
||||
(
|
||||
'include-dirs=',
|
||||
'I',
|
||||
"list of directories to search for header files" + sep_by,
|
||||
),
|
||||
('define=', 'D', "C preprocessor macros to define"),
|
||||
('undef=', 'U', "C preprocessor macros to undefine"),
|
||||
('libraries=', 'l', "external C libraries to link with"),
|
||||
(
|
||||
'library-dirs=',
|
||||
'L',
|
||||
"directories to search for external C libraries" + sep_by,
|
||||
),
|
||||
('rpath=', 'R', "directories to search for shared C libraries at runtime"),
|
||||
('link-objects=', 'O', "extra explicit link objects to include in the link"),
|
||||
('debug', 'g', "compile/link with debugging information"),
|
||||
('force', 'f', "forcibly build everything (ignore file timestamps)"),
|
||||
('compiler=', 'c', "specify the compiler type"),
|
||||
('parallel=', 'j', "number of parallel build jobs"),
|
||||
('swig-cpp', None, "make SWIG create C++ files (default is C)"),
|
||||
('swig-opts=', None, "list of SWIG command line options"),
|
||||
('swig=', None, "path to the SWIG executable"),
|
||||
('user', None, "add user include, library and rpath"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = [
|
||||
'inplace',
|
||||
'debug',
|
||||
'force',
|
||||
'swig-cpp',
|
||||
'user',
|
||||
]
|
||||
|
||||
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
|
||||
('help-compiler', None, "list available compilers", show_compilers),
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
self.extensions = None
|
||||
self.build_lib = None
|
||||
self.plat_name = None
|
||||
self.build_temp = None
|
||||
self.inplace = False
|
||||
self.package = None
|
||||
|
||||
self.include_dirs = None
|
||||
self.define = None
|
||||
self.undef = None
|
||||
self.libraries = None
|
||||
self.library_dirs = None
|
||||
self.rpath = None
|
||||
self.link_objects = None
|
||||
self.debug = None
|
||||
self.force = None
|
||||
self.compiler = None
|
||||
self.swig = None
|
||||
self.swig_cpp = None
|
||||
self.swig_opts = None
|
||||
self.user = None
|
||||
self.parallel = None
|
||||
|
||||
@staticmethod
|
||||
def _python_lib_dir(sysconfig):
|
||||
"""
|
||||
Resolve Python's library directory for building extensions
|
||||
that rely on a shared Python library.
|
||||
|
||||
See python/cpython#44264 and python/cpython#48686
|
||||
"""
|
||||
if not sysconfig.get_config_var('Py_ENABLE_SHARED'):
|
||||
return
|
||||
|
||||
if sysconfig.python_build:
|
||||
yield '.'
|
||||
return
|
||||
|
||||
if sys.platform == 'zos':
|
||||
# On z/OS, a user is not required to install Python to
|
||||
# a predetermined path, but can use Python portably
|
||||
installed_dir = sysconfig.get_config_var('base')
|
||||
lib_dir = sysconfig.get_config_var('platlibdir')
|
||||
yield os.path.join(installed_dir, lib_dir)
|
||||
else:
|
||||
# building third party extensions
|
||||
yield sysconfig.get_config_var('LIBDIR')
|
||||
|
||||
def finalize_options(self) -> None: # noqa: C901
|
||||
from distutils import sysconfig
|
||||
|
||||
self.set_undefined_options(
|
||||
'build',
|
||||
('build_lib', 'build_lib'),
|
||||
('build_temp', 'build_temp'),
|
||||
('compiler', 'compiler'),
|
||||
('debug', 'debug'),
|
||||
('force', 'force'),
|
||||
('parallel', 'parallel'),
|
||||
('plat_name', 'plat_name'),
|
||||
)
|
||||
|
||||
if self.package is None:
|
||||
self.package = self.distribution.ext_package
|
||||
|
||||
self.extensions = self.distribution.ext_modules
|
||||
|
||||
# Make sure Python's include directories (for Python.h, pyconfig.h,
|
||||
# etc.) are in the include search path.
|
||||
py_include = sysconfig.get_python_inc()
|
||||
plat_py_include = sysconfig.get_python_inc(plat_specific=True)
|
||||
if self.include_dirs is None:
|
||||
self.include_dirs = self.distribution.include_dirs or []
|
||||
if isinstance(self.include_dirs, str):
|
||||
self.include_dirs = self.include_dirs.split(os.pathsep)
|
||||
|
||||
# If in a virtualenv, add its include directory
|
||||
# Issue 16116
|
||||
if sys.exec_prefix != sys.base_exec_prefix:
|
||||
self.include_dirs.append(os.path.join(sys.exec_prefix, 'include'))
|
||||
|
||||
# Put the Python "system" include dir at the end, so that
|
||||
# any local include dirs take precedence.
|
||||
self.include_dirs.extend(py_include.split(os.path.pathsep))
|
||||
if plat_py_include != py_include:
|
||||
self.include_dirs.extend(plat_py_include.split(os.path.pathsep))
|
||||
|
||||
self.ensure_string_list('libraries')
|
||||
self.ensure_string_list('link_objects')
|
||||
|
||||
# Life is easier if we're not forever checking for None, so
|
||||
# simplify these options to empty lists if unset
|
||||
if self.libraries is None:
|
||||
self.libraries = []
|
||||
if self.library_dirs is None:
|
||||
self.library_dirs = []
|
||||
elif isinstance(self.library_dirs, str):
|
||||
self.library_dirs = self.library_dirs.split(os.pathsep)
|
||||
|
||||
if self.rpath is None:
|
||||
self.rpath = []
|
||||
elif isinstance(self.rpath, str):
|
||||
self.rpath = self.rpath.split(os.pathsep)
|
||||
|
||||
# for extensions under windows use different directories
|
||||
# for Release and Debug builds.
|
||||
# also Python's library directory must be appended to library_dirs
|
||||
if os.name == 'nt' and not is_mingw():
|
||||
# the 'libs' directory is for binary installs - we assume that
|
||||
# must be the *native* platform. But we don't really support
|
||||
# cross-compiling via a binary install anyway, so we let it go.
|
||||
self.library_dirs.append(os.path.join(sys.exec_prefix, 'libs'))
|
||||
if sys.base_exec_prefix != sys.prefix: # Issue 16116
|
||||
self.library_dirs.append(os.path.join(sys.base_exec_prefix, 'libs'))
|
||||
if self.debug:
|
||||
self.build_temp = os.path.join(self.build_temp, "Debug")
|
||||
else:
|
||||
self.build_temp = os.path.join(self.build_temp, "Release")
|
||||
|
||||
# Append the source distribution include and library directories,
|
||||
# this allows distutils on windows to work in the source tree
|
||||
self.include_dirs.append(os.path.dirname(get_config_h_filename()))
|
||||
self.library_dirs.append(sys.base_exec_prefix)
|
||||
|
||||
# Use the .lib files for the correct architecture
|
||||
if self.plat_name == 'win32':
|
||||
suffix = 'win32'
|
||||
else:
|
||||
# win-amd64
|
||||
suffix = self.plat_name[4:]
|
||||
new_lib = os.path.join(sys.exec_prefix, 'PCbuild')
|
||||
if suffix:
|
||||
new_lib = os.path.join(new_lib, suffix)
|
||||
self.library_dirs.append(new_lib)
|
||||
|
||||
# For extensions under Cygwin, Python's library directory must be
|
||||
# appended to library_dirs
|
||||
if sys.platform[:6] == 'cygwin':
|
||||
if not sysconfig.python_build:
|
||||
# building third party extensions
|
||||
self.library_dirs.append(
|
||||
os.path.join(
|
||||
sys.prefix, "lib", "python" + get_python_version(), "config"
|
||||
)
|
||||
)
|
||||
else:
|
||||
# building python standard extensions
|
||||
self.library_dirs.append('.')
|
||||
|
||||
self.library_dirs.extend(self._python_lib_dir(sysconfig))
|
||||
|
||||
# The argument parsing will result in self.define being a string, but
|
||||
# it has to be a list of 2-tuples. All the preprocessor symbols
|
||||
# specified by the 'define' option will be set to '1'. Multiple
|
||||
# symbols can be separated with commas.
|
||||
|
||||
if self.define:
|
||||
defines = self.define.split(',')
|
||||
self.define = [(symbol, '1') for symbol in defines]
|
||||
|
||||
# The option for macros to undefine is also a string from the
|
||||
# option parsing, but has to be a list. Multiple symbols can also
|
||||
# be separated with commas here.
|
||||
if self.undef:
|
||||
self.undef = self.undef.split(',')
|
||||
|
||||
if self.swig_opts is None:
|
||||
self.swig_opts = []
|
||||
else:
|
||||
self.swig_opts = self.swig_opts.split(' ')
|
||||
|
||||
# Finally add the user include and library directories if requested
|
||||
if self.user:
|
||||
user_include = os.path.join(USER_BASE, "include")
|
||||
user_lib = os.path.join(USER_BASE, "lib")
|
||||
if os.path.isdir(user_include):
|
||||
self.include_dirs.append(user_include)
|
||||
if os.path.isdir(user_lib):
|
||||
self.library_dirs.append(user_lib)
|
||||
self.rpath.append(user_lib)
|
||||
|
||||
if isinstance(self.parallel, str):
|
||||
try:
|
||||
self.parallel = int(self.parallel)
|
||||
except ValueError:
|
||||
raise DistutilsOptionError("parallel should be an integer")
|
||||
|
||||
def run(self) -> None: # noqa: C901
|
||||
# 'self.extensions', as supplied by setup.py, is a list of
|
||||
# Extension instances. See the documentation for Extension (in
|
||||
# distutils.extension) for details.
|
||||
#
|
||||
# For backwards compatibility with Distutils 0.8.2 and earlier, we
|
||||
# also allow the 'extensions' list to be a list of tuples:
|
||||
# (ext_name, build_info)
|
||||
# where build_info is a dictionary containing everything that
|
||||
# Extension instances do except the name, with a few things being
|
||||
# differently named. We convert these 2-tuples to Extension
|
||||
# instances as needed.
|
||||
|
||||
if not self.extensions:
|
||||
return
|
||||
|
||||
# If we were asked to build any C/C++ libraries, make sure that the
|
||||
# directory where we put them is in the library search path for
|
||||
# linking extensions.
|
||||
if self.distribution.has_c_libraries():
|
||||
build_clib = self.get_finalized_command('build_clib')
|
||||
self.libraries.extend(build_clib.get_library_names() or [])
|
||||
self.library_dirs.append(build_clib.build_clib)
|
||||
|
||||
# Setup the CCompiler object that we'll use to do all the
|
||||
# compiling and linking
|
||||
self.compiler = new_compiler(
|
||||
compiler=self.compiler,
|
||||
verbose=self.verbose,
|
||||
dry_run=self.dry_run,
|
||||
force=self.force,
|
||||
)
|
||||
customize_compiler(self.compiler)
|
||||
# If we are cross-compiling, init the compiler now (if we are not
|
||||
# cross-compiling, init would not hurt, but people may rely on
|
||||
# late initialization of compiler even if they shouldn't...)
|
||||
if os.name == 'nt' and self.plat_name != get_platform():
|
||||
self.compiler.initialize(self.plat_name)
|
||||
|
||||
# The official Windows free threaded Python installer doesn't set
|
||||
# Py_GIL_DISABLED because its pyconfig.h is shared with the
|
||||
# default build, so define it here (pypa/setuptools#4662).
|
||||
if os.name == 'nt' and is_freethreaded():
|
||||
self.compiler.define_macro('Py_GIL_DISABLED', '1')
|
||||
|
||||
# And make sure that any compile/link-related options (which might
|
||||
# come from the command-line or from the setup script) are set in
|
||||
# that CCompiler object -- that way, they automatically apply to
|
||||
# all compiling and linking done here.
|
||||
if self.include_dirs is not None:
|
||||
self.compiler.set_include_dirs(self.include_dirs)
|
||||
if self.define is not None:
|
||||
# 'define' option is a list of (name,value) tuples
|
||||
for name, value in self.define:
|
||||
self.compiler.define_macro(name, value)
|
||||
if self.undef is not None:
|
||||
for macro in self.undef:
|
||||
self.compiler.undefine_macro(macro)
|
||||
if self.libraries is not None:
|
||||
self.compiler.set_libraries(self.libraries)
|
||||
if self.library_dirs is not None:
|
||||
self.compiler.set_library_dirs(self.library_dirs)
|
||||
if self.rpath is not None:
|
||||
self.compiler.set_runtime_library_dirs(self.rpath)
|
||||
if self.link_objects is not None:
|
||||
self.compiler.set_link_objects(self.link_objects)
|
||||
|
||||
# Now actually compile and link everything.
|
||||
self.build_extensions()
|
||||
|
||||
def check_extensions_list(self, extensions) -> None: # noqa: C901
|
||||
"""Ensure that the list of extensions (presumably provided as a
|
||||
command option 'extensions') is valid, i.e. it is a list of
|
||||
Extension objects. We also support the old-style list of 2-tuples,
|
||||
where the tuples are (ext_name, build_info), which are converted to
|
||||
Extension instances here.
|
||||
|
||||
Raise DistutilsSetupError if the structure is invalid anywhere;
|
||||
just returns otherwise.
|
||||
"""
|
||||
if not isinstance(extensions, list):
|
||||
raise DistutilsSetupError(
|
||||
"'ext_modules' option must be a list of Extension instances"
|
||||
)
|
||||
|
||||
for i, ext in enumerate(extensions):
|
||||
if isinstance(ext, Extension):
|
||||
continue # OK! (assume type-checking done
|
||||
# by Extension constructor)
|
||||
|
||||
if not isinstance(ext, tuple) or len(ext) != 2:
|
||||
raise DistutilsSetupError(
|
||||
"each element of 'ext_modules' option must be an "
|
||||
"Extension instance or 2-tuple"
|
||||
)
|
||||
|
||||
ext_name, build_info = ext
|
||||
|
||||
log.warning(
|
||||
"old-style (ext_name, build_info) tuple found in "
|
||||
"ext_modules for extension '%s' "
|
||||
"-- please convert to Extension instance",
|
||||
ext_name,
|
||||
)
|
||||
|
||||
if not (isinstance(ext_name, str) and extension_name_re.match(ext_name)):
|
||||
raise DistutilsSetupError(
|
||||
"first element of each tuple in 'ext_modules' "
|
||||
"must be the extension name (a string)"
|
||||
)
|
||||
|
||||
if not isinstance(build_info, dict):
|
||||
raise DistutilsSetupError(
|
||||
"second element of each tuple in 'ext_modules' "
|
||||
"must be a dictionary (build info)"
|
||||
)
|
||||
|
||||
# OK, the (ext_name, build_info) dict is type-safe: convert it
|
||||
# to an Extension instance.
|
||||
ext = Extension(ext_name, build_info['sources'])
|
||||
|
||||
# Easy stuff: one-to-one mapping from dict elements to
|
||||
# instance attributes.
|
||||
for key in (
|
||||
'include_dirs',
|
||||
'library_dirs',
|
||||
'libraries',
|
||||
'extra_objects',
|
||||
'extra_compile_args',
|
||||
'extra_link_args',
|
||||
):
|
||||
val = build_info.get(key)
|
||||
if val is not None:
|
||||
setattr(ext, key, val)
|
||||
|
||||
# Medium-easy stuff: same syntax/semantics, different names.
|
||||
ext.runtime_library_dirs = build_info.get('rpath')
|
||||
if 'def_file' in build_info:
|
||||
log.warning("'def_file' element of build info dict no longer supported")
|
||||
|
||||
# Non-trivial stuff: 'macros' split into 'define_macros'
|
||||
# and 'undef_macros'.
|
||||
macros = build_info.get('macros')
|
||||
if macros:
|
||||
ext.define_macros = []
|
||||
ext.undef_macros = []
|
||||
for macro in macros:
|
||||
if not (isinstance(macro, tuple) and len(macro) in (1, 2)):
|
||||
raise DistutilsSetupError(
|
||||
"'macros' element of build info dict must be 1- or 2-tuple"
|
||||
)
|
||||
if len(macro) == 1:
|
||||
ext.undef_macros.append(macro[0])
|
||||
elif len(macro) == 2:
|
||||
ext.define_macros.append(macro)
|
||||
|
||||
extensions[i] = ext
|
||||
|
||||
def get_source_files(self):
|
||||
self.check_extensions_list(self.extensions)
|
||||
filenames = []
|
||||
|
||||
# Wouldn't it be neat if we knew the names of header files too...
|
||||
for ext in self.extensions:
|
||||
filenames.extend(ext.sources)
|
||||
return filenames
|
||||
|
||||
def get_outputs(self):
|
||||
# Sanity check the 'extensions' list -- can't assume this is being
|
||||
# done in the same run as a 'build_extensions()' call (in fact, we
|
||||
# can probably assume that it *isn't*!).
|
||||
self.check_extensions_list(self.extensions)
|
||||
|
||||
# And build the list of output (built) filenames. Note that this
|
||||
# ignores the 'inplace' flag, and assumes everything goes in the
|
||||
# "build" tree.
|
||||
return [self.get_ext_fullpath(ext.name) for ext in self.extensions]
|
||||
|
||||
def build_extensions(self) -> None:
|
||||
# First, sanity-check the 'extensions' list
|
||||
self.check_extensions_list(self.extensions)
|
||||
if self.parallel:
|
||||
self._build_extensions_parallel()
|
||||
else:
|
||||
self._build_extensions_serial()
|
||||
|
||||
def _build_extensions_parallel(self):
|
||||
workers = self.parallel
|
||||
if self.parallel is True:
|
||||
workers = os.cpu_count() # may return None
|
||||
try:
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
except ImportError:
|
||||
workers = None
|
||||
|
||||
if workers is None:
|
||||
self._build_extensions_serial()
|
||||
return
|
||||
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
futures = [
|
||||
executor.submit(self.build_extension, ext) for ext in self.extensions
|
||||
]
|
||||
for ext, fut in zip(self.extensions, futures):
|
||||
with self._filter_build_errors(ext):
|
||||
fut.result()
|
||||
|
||||
def _build_extensions_serial(self):
|
||||
for ext in self.extensions:
|
||||
with self._filter_build_errors(ext):
|
||||
self.build_extension(ext)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _filter_build_errors(self, ext):
|
||||
try:
|
||||
yield
|
||||
except (CCompilerError, DistutilsError, CompileError) as e:
|
||||
if not ext.optional:
|
||||
raise
|
||||
self.warn(f'building extension "{ext.name}" failed: {e}')
|
||||
|
||||
def build_extension(self, ext) -> None:
|
||||
sources = ext.sources
|
||||
if sources is None or not isinstance(sources, (list, tuple)):
|
||||
raise DistutilsSetupError(
|
||||
f"in 'ext_modules' option (extension '{ext.name}'), "
|
||||
"'sources' must be present and must be "
|
||||
"a list of source filenames"
|
||||
)
|
||||
# sort to make the resulting .so file build reproducible
|
||||
sources = sorted(sources)
|
||||
|
||||
ext_path = self.get_ext_fullpath(ext.name)
|
||||
depends = sources + ext.depends
|
||||
if not (self.force or newer_group(depends, ext_path, 'newer')):
|
||||
log.debug("skipping '%s' extension (up-to-date)", ext.name)
|
||||
return
|
||||
else:
|
||||
log.info("building '%s' extension", ext.name)
|
||||
|
||||
# First, scan the sources for SWIG definition files (.i), run
|
||||
# SWIG on 'em to create .c files, and modify the sources list
|
||||
# accordingly.
|
||||
sources = self.swig_sources(sources, ext)
|
||||
|
||||
# Next, compile the source code to object files.
|
||||
|
||||
# XXX not honouring 'define_macros' or 'undef_macros' -- the
|
||||
# CCompiler API needs to change to accommodate this, and I
|
||||
# want to do one thing at a time!
|
||||
|
||||
# Two possible sources for extra compiler arguments:
|
||||
# - 'extra_compile_args' in Extension object
|
||||
# - CFLAGS environment variable (not particularly
|
||||
# elegant, but people seem to expect it and I
|
||||
# guess it's useful)
|
||||
# The environment variable should take precedence, and
|
||||
# any sensible compiler will give precedence to later
|
||||
# command line args. Hence we combine them in order:
|
||||
extra_args = ext.extra_compile_args or []
|
||||
|
||||
macros = ext.define_macros[:]
|
||||
for undef in ext.undef_macros:
|
||||
macros.append((undef,))
|
||||
|
||||
objects = self.compiler.compile(
|
||||
sources,
|
||||
output_dir=self.build_temp,
|
||||
macros=macros,
|
||||
include_dirs=ext.include_dirs,
|
||||
debug=self.debug,
|
||||
extra_postargs=extra_args,
|
||||
depends=ext.depends,
|
||||
)
|
||||
|
||||
# XXX outdated variable, kept here in case third-part code
|
||||
# needs it.
|
||||
self._built_objects = objects[:]
|
||||
|
||||
# Now link the object files together into a "shared object" --
|
||||
# of course, first we have to figure out all the other things
|
||||
# that go into the mix.
|
||||
if ext.extra_objects:
|
||||
objects.extend(ext.extra_objects)
|
||||
extra_args = ext.extra_link_args or []
|
||||
|
||||
# Detect target language, if not provided
|
||||
language = ext.language or self.compiler.detect_language(sources)
|
||||
|
||||
self.compiler.link_shared_object(
|
||||
objects,
|
||||
ext_path,
|
||||
libraries=self.get_libraries(ext),
|
||||
library_dirs=ext.library_dirs,
|
||||
runtime_library_dirs=ext.runtime_library_dirs,
|
||||
extra_postargs=extra_args,
|
||||
export_symbols=self.get_export_symbols(ext),
|
||||
debug=self.debug,
|
||||
build_temp=self.build_temp,
|
||||
target_lang=language,
|
||||
)
|
||||
|
||||
def swig_sources(self, sources, extension):
|
||||
"""Walk the list of source files in 'sources', looking for SWIG
|
||||
interface (.i) files. Run SWIG on all that are found, and
|
||||
return a modified 'sources' list with SWIG source files replaced
|
||||
by the generated C (or C++) files.
|
||||
"""
|
||||
new_sources = []
|
||||
swig_sources = []
|
||||
swig_targets = {}
|
||||
|
||||
# XXX this drops generated C/C++ files into the source tree, which
|
||||
# is fine for developers who want to distribute the generated
|
||||
# source -- but there should be an option to put SWIG output in
|
||||
# the temp dir.
|
||||
|
||||
if self.swig_cpp:
|
||||
log.warning("--swig-cpp is deprecated - use --swig-opts=-c++")
|
||||
|
||||
if (
|
||||
self.swig_cpp
|
||||
or ('-c++' in self.swig_opts)
|
||||
or ('-c++' in extension.swig_opts)
|
||||
):
|
||||
target_ext = '.cpp'
|
||||
else:
|
||||
target_ext = '.c'
|
||||
|
||||
for source in sources:
|
||||
(base, ext) = os.path.splitext(source)
|
||||
if ext == ".i": # SWIG interface file
|
||||
new_sources.append(base + '_wrap' + target_ext)
|
||||
swig_sources.append(source)
|
||||
swig_targets[source] = new_sources[-1]
|
||||
else:
|
||||
new_sources.append(source)
|
||||
|
||||
if not swig_sources:
|
||||
return new_sources
|
||||
|
||||
swig = self.swig or self.find_swig()
|
||||
swig_cmd = [swig, "-python"]
|
||||
swig_cmd.extend(self.swig_opts)
|
||||
if self.swig_cpp:
|
||||
swig_cmd.append("-c++")
|
||||
|
||||
# Do not override commandline arguments
|
||||
if not self.swig_opts:
|
||||
swig_cmd.extend(extension.swig_opts)
|
||||
|
||||
for source in swig_sources:
|
||||
target = swig_targets[source]
|
||||
log.info("swigging %s to %s", source, target)
|
||||
self.spawn(swig_cmd + ["-o", target, source])
|
||||
|
||||
return new_sources
|
||||
|
||||
def find_swig(self):
|
||||
"""Return the name of the SWIG executable. On Unix, this is
|
||||
just "swig" -- it should be in the PATH. Tries a bit harder on
|
||||
Windows.
|
||||
"""
|
||||
if os.name == "posix":
|
||||
return "swig"
|
||||
elif os.name == "nt":
|
||||
# Look for SWIG in its standard installation directory on
|
||||
# Windows (or so I presume!). If we find it there, great;
|
||||
# if not, act like Unix and assume it's in the PATH.
|
||||
for vers in ("1.3", "1.2", "1.1"):
|
||||
fn = os.path.join(f"c:\\swig{vers}", "swig.exe")
|
||||
if os.path.isfile(fn):
|
||||
return fn
|
||||
else:
|
||||
return "swig.exe"
|
||||
else:
|
||||
raise DistutilsPlatformError(
|
||||
f"I don't know how to find (much less run) SWIG on platform '{os.name}'"
|
||||
)
|
||||
|
||||
# -- Name generators -----------------------------------------------
|
||||
# (extension names, filenames, whatever)
|
||||
def get_ext_fullpath(self, ext_name: str) -> str:
|
||||
"""Returns the path of the filename for a given extension.
|
||||
|
||||
The file is located in `build_lib` or directly in the package
|
||||
(inplace option).
|
||||
"""
|
||||
fullname = self.get_ext_fullname(ext_name)
|
||||
modpath = fullname.split('.')
|
||||
filename = self.get_ext_filename(modpath[-1])
|
||||
|
||||
if not self.inplace:
|
||||
# no further work needed
|
||||
# returning :
|
||||
# build_dir/package/path/filename
|
||||
filename = os.path.join(*modpath[:-1] + [filename])
|
||||
return os.path.join(self.build_lib, filename)
|
||||
|
||||
# the inplace option requires to find the package directory
|
||||
# using the build_py command for that
|
||||
package = '.'.join(modpath[0:-1])
|
||||
build_py = self.get_finalized_command('build_py')
|
||||
package_dir = os.path.abspath(build_py.get_package_dir(package))
|
||||
|
||||
# returning
|
||||
# package_dir/filename
|
||||
return os.path.join(package_dir, filename)
|
||||
|
||||
def get_ext_fullname(self, ext_name: str) -> str:
|
||||
"""Returns the fullname of a given extension name.
|
||||
|
||||
Adds the `package.` prefix"""
|
||||
if self.package is None:
|
||||
return ext_name
|
||||
else:
|
||||
return self.package + '.' + ext_name
|
||||
|
||||
def get_ext_filename(self, ext_name: str) -> str:
|
||||
r"""Convert the name of an extension (eg. "foo.bar") into the name
|
||||
of the file from which it will be loaded (eg. "foo/bar.so", or
|
||||
"foo\bar.pyd").
|
||||
"""
|
||||
from ..sysconfig import get_config_var
|
||||
|
||||
ext_path = ext_name.split('.')
|
||||
ext_suffix = get_config_var('EXT_SUFFIX')
|
||||
return os.path.join(*ext_path) + ext_suffix
|
||||
|
||||
def get_export_symbols(self, ext: Extension) -> list[str]:
|
||||
"""Return the list of symbols that a shared extension has to
|
||||
export. This either uses 'ext.export_symbols' or, if it's not
|
||||
provided, "PyInit_" + module_name. Only relevant on Windows, where
|
||||
the .pyd file (DLL) must export the module "PyInit_" function.
|
||||
"""
|
||||
name = self._get_module_name_for_symbol(ext)
|
||||
try:
|
||||
# Unicode module name support as defined in PEP-489
|
||||
# https://peps.python.org/pep-0489/#export-hook-name
|
||||
name.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
suffix = 'U_' + name.encode('punycode').replace(b'-', b'_').decode('ascii')
|
||||
else:
|
||||
suffix = "_" + name
|
||||
|
||||
initfunc_name = "PyInit" + suffix
|
||||
if initfunc_name not in ext.export_symbols:
|
||||
ext.export_symbols.append(initfunc_name)
|
||||
return ext.export_symbols
|
||||
|
||||
def _get_module_name_for_symbol(self, ext):
|
||||
# Package name should be used for `__init__` modules
|
||||
# https://github.com/python/cpython/issues/80074
|
||||
# https://github.com/pypa/setuptools/issues/4826
|
||||
parts = ext.name.split(".")
|
||||
if parts[-1] == "__init__" and len(parts) >= 2:
|
||||
return parts[-2]
|
||||
return parts[-1]
|
||||
|
||||
def get_libraries(self, ext: Extension) -> list[str]: # noqa: C901
|
||||
"""Return the list of libraries to link against when building a
|
||||
shared extension. On most platforms, this is just 'ext.libraries';
|
||||
on Windows, we add the Python library (eg. python20.dll).
|
||||
"""
|
||||
# The python library is always needed on Windows. For MSVC, this
|
||||
# is redundant, since the library is mentioned in a pragma in
|
||||
# pyconfig.h that MSVC groks. The other Windows compilers all seem
|
||||
# to need it mentioned explicitly, though, so that's what we do.
|
||||
# Append '_d' to the python import library on debug builds.
|
||||
if sys.platform == "win32" and not is_mingw():
|
||||
from .._msvccompiler import MSVCCompiler
|
||||
|
||||
if not isinstance(self.compiler, MSVCCompiler):
|
||||
template = "python%d%d"
|
||||
if self.debug:
|
||||
template = template + '_d'
|
||||
pythonlib = template % (
|
||||
sys.hexversion >> 24,
|
||||
(sys.hexversion >> 16) & 0xFF,
|
||||
)
|
||||
# don't extend ext.libraries, it may be shared with other
|
||||
# extensions, it is a reference to the original list
|
||||
return ext.libraries + [pythonlib]
|
||||
else:
|
||||
# On Android only the main executable and LD_PRELOADs are considered
|
||||
# to be RTLD_GLOBAL, all the dependencies of the main executable
|
||||
# remain RTLD_LOCAL and so the shared libraries must be linked with
|
||||
# libpython when python is built with a shared python library (issue
|
||||
# bpo-21536).
|
||||
# On Cygwin (and if required, other POSIX-like platforms based on
|
||||
# Windows like MinGW) it is simply necessary that all symbols in
|
||||
# shared libraries are resolved at link time.
|
||||
from ..sysconfig import get_config_var
|
||||
|
||||
link_libpython = False
|
||||
if get_config_var('Py_ENABLE_SHARED'):
|
||||
# A native build on an Android device or on Cygwin
|
||||
if hasattr(sys, 'getandroidapilevel'):
|
||||
link_libpython = True
|
||||
elif sys.platform == 'cygwin' or is_mingw():
|
||||
link_libpython = True
|
||||
elif '_PYTHON_HOST_PLATFORM' in os.environ:
|
||||
# We are cross-compiling for one of the relevant platforms
|
||||
if get_config_var('ANDROID_API_LEVEL') != 0:
|
||||
link_libpython = True
|
||||
elif get_config_var('MACHDEP') == 'cygwin':
|
||||
link_libpython = True
|
||||
|
||||
if link_libpython:
|
||||
ldversion = get_config_var('LDVERSION')
|
||||
return ext.libraries + ['python' + ldversion]
|
||||
|
||||
return ext.libraries
|
||||
@@ -0,0 +1,407 @@
|
||||
"""distutils.command.build_py
|
||||
|
||||
Implements the Distutils 'build_py' command."""
|
||||
|
||||
import glob
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from distutils._log import log
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..errors import DistutilsFileError, DistutilsOptionError
|
||||
from ..util import convert_path
|
||||
|
||||
|
||||
class build_py(Command):
|
||||
description = "\"build\" pure Python modules (copy to build directory)"
|
||||
|
||||
user_options = [
|
||||
('build-lib=', 'd', "directory to \"build\" (copy) to"),
|
||||
('compile', 'c', "compile .py to .pyc"),
|
||||
('no-compile', None, "don't compile .py files [default]"),
|
||||
(
|
||||
'optimize=',
|
||||
'O',
|
||||
"also compile with optimization: -O1 for \"python -O\", "
|
||||
"-O2 for \"python -OO\", and -O0 to disable [default: -O0]",
|
||||
),
|
||||
('force', 'f', "forcibly build everything (ignore file timestamps)"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['compile', 'force']
|
||||
negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
|
||||
|
||||
def initialize_options(self):
|
||||
self.build_lib = None
|
||||
self.py_modules = None
|
||||
self.package = None
|
||||
self.package_data = None
|
||||
self.package_dir = None
|
||||
self.compile = False
|
||||
self.optimize = 0
|
||||
self.force = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
self.set_undefined_options(
|
||||
'build', ('build_lib', 'build_lib'), ('force', 'force')
|
||||
)
|
||||
|
||||
# Get the distribution options that are aliases for build_py
|
||||
# options -- list of packages and list of modules.
|
||||
self.packages = self.distribution.packages
|
||||
self.py_modules = self.distribution.py_modules
|
||||
self.package_data = self.distribution.package_data
|
||||
self.package_dir = {}
|
||||
if self.distribution.package_dir:
|
||||
for name, path in self.distribution.package_dir.items():
|
||||
self.package_dir[name] = convert_path(path)
|
||||
self.data_files = self.get_data_files()
|
||||
|
||||
# Ick, copied straight from install_lib.py (fancy_getopt needs a
|
||||
# type system! Hell, *everything* needs a type system!!!)
|
||||
if not isinstance(self.optimize, int):
|
||||
try:
|
||||
self.optimize = int(self.optimize)
|
||||
assert 0 <= self.optimize <= 2
|
||||
except (ValueError, AssertionError):
|
||||
raise DistutilsOptionError("optimize must be 0, 1, or 2")
|
||||
|
||||
def run(self) -> None:
|
||||
# XXX copy_file by default preserves atime and mtime. IMHO this is
|
||||
# the right thing to do, but perhaps it should be an option -- in
|
||||
# particular, a site administrator might want installed files to
|
||||
# reflect the time of installation rather than the last
|
||||
# modification time before the installed release.
|
||||
|
||||
# XXX copy_file by default preserves mode, which appears to be the
|
||||
# wrong thing to do: if a file is read-only in the working
|
||||
# directory, we want it to be installed read/write so that the next
|
||||
# installation of the same module distribution can overwrite it
|
||||
# without problems. (This might be a Unix-specific issue.) Thus
|
||||
# we turn off 'preserve_mode' when copying to the build directory,
|
||||
# since the build directory is supposed to be exactly what the
|
||||
# installation will look like (ie. we preserve mode when
|
||||
# installing).
|
||||
|
||||
# Two options control which modules will be installed: 'packages'
|
||||
# and 'py_modules'. The former lets us work with whole packages, not
|
||||
# specifying individual modules at all; the latter is for
|
||||
# specifying modules one-at-a-time.
|
||||
|
||||
if self.py_modules:
|
||||
self.build_modules()
|
||||
if self.packages:
|
||||
self.build_packages()
|
||||
self.build_package_data()
|
||||
|
||||
self.byte_compile(self.get_outputs(include_bytecode=False))
|
||||
|
||||
def get_data_files(self):
|
||||
"""Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
|
||||
data = []
|
||||
if not self.packages:
|
||||
return data
|
||||
for package in self.packages:
|
||||
# Locate package source directory
|
||||
src_dir = self.get_package_dir(package)
|
||||
|
||||
# Compute package build directory
|
||||
build_dir = os.path.join(*([self.build_lib] + package.split('.')))
|
||||
|
||||
# Length of path to strip from found files
|
||||
plen = 0
|
||||
if src_dir:
|
||||
plen = len(src_dir) + 1
|
||||
|
||||
# Strip directory from globbed filenames
|
||||
filenames = [file[plen:] for file in self.find_data_files(package, src_dir)]
|
||||
data.append((package, src_dir, build_dir, filenames))
|
||||
return data
|
||||
|
||||
def find_data_files(self, package, src_dir):
|
||||
"""Return filenames for package's data files in 'src_dir'"""
|
||||
globs = self.package_data.get('', []) + self.package_data.get(package, [])
|
||||
files = []
|
||||
for pattern in globs:
|
||||
# Each pattern has to be converted to a platform-specific path
|
||||
filelist = glob.glob(
|
||||
os.path.join(glob.escape(src_dir), convert_path(pattern))
|
||||
)
|
||||
# Files that match more than one pattern are only added once
|
||||
files.extend([
|
||||
fn for fn in filelist if fn not in files and os.path.isfile(fn)
|
||||
])
|
||||
return files
|
||||
|
||||
def build_package_data(self) -> None:
|
||||
"""Copy data files into build directory"""
|
||||
for _package, src_dir, build_dir, filenames in self.data_files:
|
||||
for filename in filenames:
|
||||
target = os.path.join(build_dir, filename)
|
||||
self.mkpath(os.path.dirname(target))
|
||||
self.copy_file(
|
||||
os.path.join(src_dir, filename), target, preserve_mode=False
|
||||
)
|
||||
|
||||
def get_package_dir(self, package):
|
||||
"""Return the directory, relative to the top of the source
|
||||
distribution, where package 'package' should be found
|
||||
(at least according to the 'package_dir' option, if any)."""
|
||||
path = package.split('.')
|
||||
|
||||
if not self.package_dir:
|
||||
if path:
|
||||
return os.path.join(*path)
|
||||
else:
|
||||
return ''
|
||||
else:
|
||||
tail = []
|
||||
while path:
|
||||
try:
|
||||
pdir = self.package_dir['.'.join(path)]
|
||||
except KeyError:
|
||||
tail.insert(0, path[-1])
|
||||
del path[-1]
|
||||
else:
|
||||
tail.insert(0, pdir)
|
||||
return os.path.join(*tail)
|
||||
else:
|
||||
# Oops, got all the way through 'path' without finding a
|
||||
# match in package_dir. If package_dir defines a directory
|
||||
# for the root (nameless) package, then fallback on it;
|
||||
# otherwise, we might as well have not consulted
|
||||
# package_dir at all, as we just use the directory implied
|
||||
# by 'tail' (which should be the same as the original value
|
||||
# of 'path' at this point).
|
||||
pdir = self.package_dir.get('')
|
||||
if pdir is not None:
|
||||
tail.insert(0, pdir)
|
||||
|
||||
if tail:
|
||||
return os.path.join(*tail)
|
||||
else:
|
||||
return ''
|
||||
|
||||
def check_package(self, package, package_dir):
|
||||
# Empty dir name means current directory, which we can probably
|
||||
# assume exists. Also, os.path.exists and isdir don't know about
|
||||
# my "empty string means current dir" convention, so we have to
|
||||
# circumvent them.
|
||||
if package_dir != "":
|
||||
if not os.path.exists(package_dir):
|
||||
raise DistutilsFileError(
|
||||
f"package directory '{package_dir}' does not exist"
|
||||
)
|
||||
if not os.path.isdir(package_dir):
|
||||
raise DistutilsFileError(
|
||||
f"supposed package directory '{package_dir}' exists, "
|
||||
"but is not a directory"
|
||||
)
|
||||
|
||||
# Directories without __init__.py are namespace packages (PEP 420).
|
||||
if package:
|
||||
init_py = os.path.join(package_dir, "__init__.py")
|
||||
if os.path.isfile(init_py):
|
||||
return init_py
|
||||
|
||||
# Either not in a package at all (__init__.py not expected), or
|
||||
# __init__.py doesn't exist -- so don't return the filename.
|
||||
return None
|
||||
|
||||
def check_module(self, module, module_file):
|
||||
if not os.path.isfile(module_file):
|
||||
log.warning("file %s (for module %s) not found", module_file, module)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def find_package_modules(self, package, package_dir):
|
||||
self.check_package(package, package_dir)
|
||||
module_files = glob.glob(os.path.join(glob.escape(package_dir), "*.py"))
|
||||
modules = []
|
||||
setup_script = os.path.abspath(self.distribution.script_name)
|
||||
|
||||
for f in module_files:
|
||||
abs_f = os.path.abspath(f)
|
||||
if abs_f != setup_script:
|
||||
module = os.path.splitext(os.path.basename(f))[0]
|
||||
modules.append((package, module, f))
|
||||
else:
|
||||
self.debug_print(f"excluding {setup_script}")
|
||||
return modules
|
||||
|
||||
def find_modules(self):
|
||||
"""Finds individually-specified Python modules, ie. those listed by
|
||||
module name in 'self.py_modules'. Returns a list of tuples (package,
|
||||
module_base, filename): 'package' is a tuple of the path through
|
||||
package-space to the module; 'module_base' is the bare (no
|
||||
packages, no dots) module name, and 'filename' is the path to the
|
||||
".py" file (relative to the distribution root) that implements the
|
||||
module.
|
||||
"""
|
||||
# Map package names to tuples of useful info about the package:
|
||||
# (package_dir, checked)
|
||||
# package_dir - the directory where we'll find source files for
|
||||
# this package
|
||||
# checked - true if we have checked that the package directory
|
||||
# is valid (exists, contains __init__.py, ... ?)
|
||||
packages = {}
|
||||
|
||||
# List of (package, module, filename) tuples to return
|
||||
modules = []
|
||||
|
||||
# We treat modules-in-packages almost the same as toplevel modules,
|
||||
# just the "package" for a toplevel is empty (either an empty
|
||||
# string or empty list, depending on context). Differences:
|
||||
# - don't check for __init__.py in directory for empty package
|
||||
for module in self.py_modules:
|
||||
path = module.split('.')
|
||||
package = '.'.join(path[0:-1])
|
||||
module_base = path[-1]
|
||||
|
||||
try:
|
||||
(package_dir, checked) = packages[package]
|
||||
except KeyError:
|
||||
package_dir = self.get_package_dir(package)
|
||||
checked = False
|
||||
|
||||
if not checked:
|
||||
init_py = self.check_package(package, package_dir)
|
||||
packages[package] = (package_dir, 1)
|
||||
if init_py:
|
||||
modules.append((package, "__init__", init_py))
|
||||
|
||||
# XXX perhaps we should also check for just .pyc files
|
||||
# (so greedy closed-source bastards can distribute Python
|
||||
# modules too)
|
||||
module_file = os.path.join(package_dir, module_base + ".py")
|
||||
if not self.check_module(module, module_file):
|
||||
continue
|
||||
|
||||
modules.append((package, module_base, module_file))
|
||||
|
||||
return modules
|
||||
|
||||
def find_all_modules(self):
|
||||
"""Compute the list of all modules that will be built, whether
|
||||
they are specified one-module-at-a-time ('self.py_modules') or
|
||||
by whole packages ('self.packages'). Return a list of tuples
|
||||
(package, module, module_file), just like 'find_modules()' and
|
||||
'find_package_modules()' do."""
|
||||
modules = []
|
||||
if self.py_modules:
|
||||
modules.extend(self.find_modules())
|
||||
if self.packages:
|
||||
for package in self.packages:
|
||||
package_dir = self.get_package_dir(package)
|
||||
m = self.find_package_modules(package, package_dir)
|
||||
modules.extend(m)
|
||||
return modules
|
||||
|
||||
def get_source_files(self):
|
||||
return [module[-1] for module in self.find_all_modules()]
|
||||
|
||||
def get_module_outfile(self, build_dir, package, module):
|
||||
outfile_path = [build_dir] + list(package) + [module + ".py"]
|
||||
return os.path.join(*outfile_path)
|
||||
|
||||
def get_outputs(self, include_bytecode: bool = True) -> list[str]:
|
||||
modules = self.find_all_modules()
|
||||
outputs = []
|
||||
for package, module, _module_file in modules:
|
||||
package = package.split('.')
|
||||
filename = self.get_module_outfile(self.build_lib, package, module)
|
||||
outputs.append(filename)
|
||||
if include_bytecode:
|
||||
if self.compile:
|
||||
outputs.append(
|
||||
importlib.util.cache_from_source(filename, optimization='')
|
||||
)
|
||||
if self.optimize > 0:
|
||||
outputs.append(
|
||||
importlib.util.cache_from_source(
|
||||
filename, optimization=self.optimize
|
||||
)
|
||||
)
|
||||
|
||||
outputs += [
|
||||
os.path.join(build_dir, filename)
|
||||
for package, src_dir, build_dir, filenames in self.data_files
|
||||
for filename in filenames
|
||||
]
|
||||
|
||||
return outputs
|
||||
|
||||
def build_module(self, module, module_file, package):
|
||||
if isinstance(package, str):
|
||||
package = package.split('.')
|
||||
elif not isinstance(package, (list, tuple)):
|
||||
raise TypeError(
|
||||
"'package' must be a string (dot-separated), list, or tuple"
|
||||
)
|
||||
|
||||
# Now put the module source file into the "build" area -- this is
|
||||
# easy, we just copy it somewhere under self.build_lib (the build
|
||||
# directory for Python source).
|
||||
outfile = self.get_module_outfile(self.build_lib, package, module)
|
||||
dir = os.path.dirname(outfile)
|
||||
self.mkpath(dir)
|
||||
return self.copy_file(module_file, outfile, preserve_mode=False)
|
||||
|
||||
def build_modules(self) -> None:
|
||||
modules = self.find_modules()
|
||||
for package, module, module_file in modules:
|
||||
# Now "build" the module -- ie. copy the source file to
|
||||
# self.build_lib (the build directory for Python source).
|
||||
# (Actually, it gets copied to the directory for this package
|
||||
# under self.build_lib.)
|
||||
self.build_module(module, module_file, package)
|
||||
|
||||
def build_packages(self) -> None:
|
||||
for package in self.packages:
|
||||
# Get list of (package, module, module_file) tuples based on
|
||||
# scanning the package directory. 'package' is only included
|
||||
# in the tuple so that 'find_modules()' and
|
||||
# 'find_package_tuples()' have a consistent interface; it's
|
||||
# ignored here (apart from a sanity check). Also, 'module' is
|
||||
# the *unqualified* module name (ie. no dots, no package -- we
|
||||
# already know its package!), and 'module_file' is the path to
|
||||
# the .py file, relative to the current directory
|
||||
# (ie. including 'package_dir').
|
||||
package_dir = self.get_package_dir(package)
|
||||
modules = self.find_package_modules(package, package_dir)
|
||||
|
||||
# Now loop over the modules we found, "building" each one (just
|
||||
# copy it to self.build_lib).
|
||||
for package_, module, module_file in modules:
|
||||
assert package == package_
|
||||
self.build_module(module, module_file, package)
|
||||
|
||||
def byte_compile(self, files) -> None:
|
||||
if sys.dont_write_bytecode:
|
||||
self.warn('byte-compiling is disabled, skipping.')
|
||||
return
|
||||
|
||||
from ..util import byte_compile
|
||||
|
||||
prefix = self.build_lib
|
||||
if prefix[-1] != os.sep:
|
||||
prefix = prefix + os.sep
|
||||
|
||||
# XXX this code is essentially the same as the 'byte_compile()
|
||||
# method of the "install_lib" command, except for the determination
|
||||
# of the 'prefix' string. Hmmm.
|
||||
if self.compile:
|
||||
byte_compile(
|
||||
files, optimize=0, force=self.force, prefix=prefix, dry_run=self.dry_run
|
||||
)
|
||||
if self.optimize > 0:
|
||||
byte_compile(
|
||||
files,
|
||||
optimize=self.optimize,
|
||||
force=self.force,
|
||||
prefix=prefix,
|
||||
dry_run=self.dry_run,
|
||||
)
|
||||
@@ -0,0 +1,160 @@
|
||||
"""distutils.command.build_scripts
|
||||
|
||||
Implements the Distutils 'build_scripts' command."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tokenize
|
||||
from distutils._log import log
|
||||
from stat import ST_MODE
|
||||
from typing import ClassVar
|
||||
|
||||
from .._modified import newer
|
||||
from ..core import Command
|
||||
from ..util import convert_path
|
||||
|
||||
shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$')
|
||||
"""
|
||||
Pattern matching a Python interpreter indicated in first line of a script.
|
||||
"""
|
||||
|
||||
# for Setuptools compatibility
|
||||
first_line_re = shebang_pattern
|
||||
|
||||
|
||||
class build_scripts(Command):
|
||||
description = "\"build\" scripts (copy and fixup #! line)"
|
||||
|
||||
user_options: ClassVar[list[tuple[str, str, str]]] = [
|
||||
('build-dir=', 'd', "directory to \"build\" (copy) to"),
|
||||
('force', 'f', "forcibly build everything (ignore file timestamps"),
|
||||
('executable=', 'e', "specify final destination interpreter path"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['force']
|
||||
|
||||
def initialize_options(self):
|
||||
self.build_dir = None
|
||||
self.scripts = None
|
||||
self.force = None
|
||||
self.executable = None
|
||||
|
||||
def finalize_options(self):
|
||||
self.set_undefined_options(
|
||||
'build',
|
||||
('build_scripts', 'build_dir'),
|
||||
('force', 'force'),
|
||||
('executable', 'executable'),
|
||||
)
|
||||
self.scripts = self.distribution.scripts
|
||||
|
||||
def get_source_files(self):
|
||||
return self.scripts
|
||||
|
||||
def run(self):
|
||||
if not self.scripts:
|
||||
return
|
||||
self.copy_scripts()
|
||||
|
||||
def copy_scripts(self):
|
||||
"""
|
||||
Copy each script listed in ``self.scripts``.
|
||||
|
||||
If a script is marked as a Python script (first line matches
|
||||
'shebang_pattern', i.e. starts with ``#!`` and contains
|
||||
"python"), then adjust in the copy the first line to refer to
|
||||
the current Python interpreter.
|
||||
"""
|
||||
self.mkpath(self.build_dir)
|
||||
outfiles = []
|
||||
updated_files = []
|
||||
for script in self.scripts:
|
||||
self._copy_script(script, outfiles, updated_files)
|
||||
|
||||
self._change_modes(outfiles)
|
||||
|
||||
return outfiles, updated_files
|
||||
|
||||
def _copy_script(self, script, outfiles, updated_files):
|
||||
shebang_match = None
|
||||
script = convert_path(script)
|
||||
outfile = os.path.join(self.build_dir, os.path.basename(script))
|
||||
outfiles.append(outfile)
|
||||
|
||||
if not self.force and not newer(script, outfile):
|
||||
log.debug("not copying %s (up-to-date)", script)
|
||||
return
|
||||
|
||||
# Always open the file, but ignore failures in dry-run mode
|
||||
# in order to attempt to copy directly.
|
||||
try:
|
||||
f = tokenize.open(script)
|
||||
except OSError:
|
||||
if not self.dry_run:
|
||||
raise
|
||||
f = None
|
||||
else:
|
||||
first_line = f.readline()
|
||||
if not first_line:
|
||||
self.warn(f"{script} is an empty file (skipping)")
|
||||
return
|
||||
|
||||
shebang_match = shebang_pattern.match(first_line)
|
||||
|
||||
updated_files.append(outfile)
|
||||
if shebang_match:
|
||||
log.info("copying and adjusting %s -> %s", script, self.build_dir)
|
||||
if not self.dry_run:
|
||||
post_interp = shebang_match.group(1) or ''
|
||||
shebang = f"#!python{post_interp}\n"
|
||||
self._validate_shebang(shebang, f.encoding)
|
||||
with open(outfile, "w", encoding=f.encoding) as outf:
|
||||
outf.write(shebang)
|
||||
outf.writelines(f.readlines())
|
||||
if f:
|
||||
f.close()
|
||||
else:
|
||||
if f:
|
||||
f.close()
|
||||
self.copy_file(script, outfile)
|
||||
|
||||
def _change_modes(self, outfiles):
|
||||
if os.name != 'posix':
|
||||
return
|
||||
|
||||
for file in outfiles:
|
||||
self._change_mode(file)
|
||||
|
||||
def _change_mode(self, file):
|
||||
if self.dry_run:
|
||||
log.info("changing mode of %s", file)
|
||||
return
|
||||
|
||||
oldmode = os.stat(file)[ST_MODE] & 0o7777
|
||||
newmode = (oldmode | 0o555) & 0o7777
|
||||
if newmode != oldmode:
|
||||
log.info("changing mode of %s from %o to %o", file, oldmode, newmode)
|
||||
os.chmod(file, newmode)
|
||||
|
||||
@staticmethod
|
||||
def _validate_shebang(shebang, encoding):
|
||||
# Python parser starts to read a script using UTF-8 until
|
||||
# it gets a #coding:xxx cookie. The shebang has to be the
|
||||
# first line of a file, the #coding:xxx cookie cannot be
|
||||
# written before. So the shebang has to be encodable to
|
||||
# UTF-8.
|
||||
try:
|
||||
shebang.encode('utf-8')
|
||||
except UnicodeEncodeError:
|
||||
raise ValueError(f"The shebang ({shebang!r}) is not encodable to utf-8")
|
||||
|
||||
# If the script is encoded to a custom encoding (use a
|
||||
# #coding:xxx cookie), the shebang has to be encodable to
|
||||
# the script encoding too.
|
||||
try:
|
||||
shebang.encode(encoding)
|
||||
except UnicodeEncodeError:
|
||||
raise ValueError(
|
||||
f"The shebang ({shebang!r}) is not encodable "
|
||||
f"to the script encoding ({encoding})"
|
||||
)
|
||||
152
.venv/Lib/site-packages/setuptools/_distutils/command/check.py
Normal file
152
.venv/Lib/site-packages/setuptools/_distutils/command/check.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""distutils.command.check
|
||||
|
||||
Implements the Distutils 'check' command.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..errors import DistutilsSetupError
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
import docutils.frontend
|
||||
import docutils.nodes
|
||||
import docutils.parsers.rst
|
||||
import docutils.utils
|
||||
|
||||
class SilentReporter(docutils.utils.Reporter):
|
||||
def __init__(
|
||||
self,
|
||||
source,
|
||||
report_level,
|
||||
halt_level,
|
||||
stream=None,
|
||||
debug=False,
|
||||
encoding='ascii',
|
||||
error_handler='replace',
|
||||
):
|
||||
self.messages = []
|
||||
super().__init__(
|
||||
source, report_level, halt_level, stream, debug, encoding, error_handler
|
||||
)
|
||||
|
||||
def system_message(self, level, message, *children, **kwargs):
|
||||
self.messages.append((level, message, children, kwargs))
|
||||
return docutils.nodes.system_message(
|
||||
message, *children, level=level, type=self.levels[level], **kwargs
|
||||
)
|
||||
|
||||
|
||||
class check(Command):
|
||||
"""This command checks the meta-data of the package."""
|
||||
|
||||
description = "perform some checks on the package"
|
||||
user_options: ClassVar[list[tuple[str, str, str]]] = [
|
||||
('metadata', 'm', 'Verify meta-data'),
|
||||
(
|
||||
'restructuredtext',
|
||||
'r',
|
||||
'Checks if long string meta-data syntax are reStructuredText-compliant',
|
||||
),
|
||||
('strict', 's', 'Will exit with an error if a check fails'),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['metadata', 'restructuredtext', 'strict']
|
||||
|
||||
def initialize_options(self):
|
||||
"""Sets default values for options."""
|
||||
self.restructuredtext = False
|
||||
self.metadata = 1
|
||||
self.strict = False
|
||||
self._warnings = 0
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def warn(self, msg):
|
||||
"""Counts the number of warnings that occurs."""
|
||||
self._warnings += 1
|
||||
return Command.warn(self, msg)
|
||||
|
||||
def run(self):
|
||||
"""Runs the command."""
|
||||
# perform the various tests
|
||||
if self.metadata:
|
||||
self.check_metadata()
|
||||
if self.restructuredtext:
|
||||
if 'docutils' in globals():
|
||||
try:
|
||||
self.check_restructuredtext()
|
||||
except TypeError as exc:
|
||||
raise DistutilsSetupError(str(exc))
|
||||
elif self.strict:
|
||||
raise DistutilsSetupError('The docutils package is needed.')
|
||||
|
||||
# let's raise an error in strict mode, if we have at least
|
||||
# one warning
|
||||
if self.strict and self._warnings > 0:
|
||||
raise DistutilsSetupError('Please correct your package.')
|
||||
|
||||
def check_metadata(self):
|
||||
"""Ensures that all required elements of meta-data are supplied.
|
||||
|
||||
Required fields:
|
||||
name, version
|
||||
|
||||
Warns if any are missing.
|
||||
"""
|
||||
metadata = self.distribution.metadata
|
||||
|
||||
missing = [
|
||||
attr for attr in ('name', 'version') if not getattr(metadata, attr, None)
|
||||
]
|
||||
|
||||
if missing:
|
||||
self.warn("missing required meta-data: {}".format(', '.join(missing)))
|
||||
|
||||
def check_restructuredtext(self):
|
||||
"""Checks if the long string fields are reST-compliant."""
|
||||
data = self.distribution.get_long_description()
|
||||
for warning in self._check_rst_data(data):
|
||||
line = warning[-1].get('line')
|
||||
if line is None:
|
||||
warning = warning[1]
|
||||
else:
|
||||
warning = f'{warning[1]} (line {line})'
|
||||
self.warn(warning)
|
||||
|
||||
def _check_rst_data(self, data):
|
||||
"""Returns warnings when the provided data doesn't compile."""
|
||||
# the include and csv_table directives need this to be a path
|
||||
source_path = self.distribution.script_name or 'setup.py'
|
||||
parser = docutils.parsers.rst.Parser()
|
||||
settings = docutils.frontend.OptionParser(
|
||||
components=(docutils.parsers.rst.Parser,)
|
||||
).get_default_values()
|
||||
settings.tab_width = 4
|
||||
settings.pep_references = None
|
||||
settings.rfc_references = None
|
||||
reporter = SilentReporter(
|
||||
source_path,
|
||||
settings.report_level,
|
||||
settings.halt_level,
|
||||
stream=settings.warning_stream,
|
||||
debug=settings.debug,
|
||||
encoding=settings.error_encoding,
|
||||
error_handler=settings.error_encoding_error_handler,
|
||||
)
|
||||
|
||||
document = docutils.nodes.document(settings, reporter, source=source_path)
|
||||
document.note_source(source_path, -1)
|
||||
try:
|
||||
parser.parse(data, document)
|
||||
except (AttributeError, TypeError) as e:
|
||||
reporter.messages.append((
|
||||
-1,
|
||||
f'Could not finish the parsing: {e}.',
|
||||
'',
|
||||
{},
|
||||
))
|
||||
|
||||
return reporter.messages
|
||||
@@ -0,0 +1,77 @@
|
||||
"""distutils.command.clean
|
||||
|
||||
Implements the Distutils 'clean' command."""
|
||||
|
||||
# contributed by Bastian Kleineidam <calvin@cs.uni-sb.de>, added 2000-03-18
|
||||
|
||||
import os
|
||||
from distutils._log import log
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..dir_util import remove_tree
|
||||
|
||||
|
||||
class clean(Command):
|
||||
description = "clean up temporary files from 'build' command"
|
||||
user_options = [
|
||||
('build-base=', 'b', "base build directory [default: 'build.build-base']"),
|
||||
(
|
||||
'build-lib=',
|
||||
None,
|
||||
"build directory for all modules [default: 'build.build-lib']",
|
||||
),
|
||||
('build-temp=', 't', "temporary build directory [default: 'build.build-temp']"),
|
||||
(
|
||||
'build-scripts=',
|
||||
None,
|
||||
"build directory for scripts [default: 'build.build-scripts']",
|
||||
),
|
||||
('bdist-base=', None, "temporary directory for built distributions"),
|
||||
('all', 'a', "remove all build output, not just temporary by-products"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['all']
|
||||
|
||||
def initialize_options(self):
|
||||
self.build_base = None
|
||||
self.build_lib = None
|
||||
self.build_temp = None
|
||||
self.build_scripts = None
|
||||
self.bdist_base = None
|
||||
self.all = None
|
||||
|
||||
def finalize_options(self):
|
||||
self.set_undefined_options(
|
||||
'build',
|
||||
('build_base', 'build_base'),
|
||||
('build_lib', 'build_lib'),
|
||||
('build_scripts', 'build_scripts'),
|
||||
('build_temp', 'build_temp'),
|
||||
)
|
||||
self.set_undefined_options('bdist', ('bdist_base', 'bdist_base'))
|
||||
|
||||
def run(self):
|
||||
# remove the build/temp.<plat> directory (unless it's already
|
||||
# gone)
|
||||
if os.path.exists(self.build_temp):
|
||||
remove_tree(self.build_temp, dry_run=self.dry_run)
|
||||
else:
|
||||
log.debug("'%s' does not exist -- can't clean it", self.build_temp)
|
||||
|
||||
if self.all:
|
||||
# remove build directories
|
||||
for directory in (self.build_lib, self.bdist_base, self.build_scripts):
|
||||
if os.path.exists(directory):
|
||||
remove_tree(directory, dry_run=self.dry_run)
|
||||
else:
|
||||
log.warning("'%s' does not exist -- can't clean it", directory)
|
||||
|
||||
# just for the heck of it, try to remove the base build directory:
|
||||
# we might have emptied it right now, but if not we don't care
|
||||
if not self.dry_run:
|
||||
try:
|
||||
os.rmdir(self.build_base)
|
||||
log.info("removing '%s'", self.build_base)
|
||||
except OSError:
|
||||
pass
|
||||
358
.venv/Lib/site-packages/setuptools/_distutils/command/config.py
Normal file
358
.venv/Lib/site-packages/setuptools/_distutils/command/config.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""distutils.command.config
|
||||
|
||||
Implements the Distutils 'config' command, a (mostly) empty command class
|
||||
that exists mainly to be sub-classed by specific module distributions and
|
||||
applications. The idea is that while every "config" command is different,
|
||||
at least they're all named the same, and users always see "config" in the
|
||||
list of standard commands. Also, this is a good place to put common
|
||||
configure-like tasks: "try to compile this C code", or "figure out where
|
||||
this header file lives".
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from distutils._log import log
|
||||
|
||||
from ..ccompiler import CCompiler, CompileError, LinkError, new_compiler
|
||||
from ..core import Command
|
||||
from ..errors import DistutilsExecError
|
||||
from ..sysconfig import customize_compiler
|
||||
|
||||
LANG_EXT = {"c": ".c", "c++": ".cxx"}
|
||||
|
||||
|
||||
class config(Command):
|
||||
description = "prepare to build"
|
||||
|
||||
user_options = [
|
||||
('compiler=', None, "specify the compiler type"),
|
||||
('cc=', None, "specify the compiler executable"),
|
||||
('include-dirs=', 'I', "list of directories to search for header files"),
|
||||
('define=', 'D', "C preprocessor macros to define"),
|
||||
('undef=', 'U', "C preprocessor macros to undefine"),
|
||||
('libraries=', 'l', "external C libraries to link with"),
|
||||
('library-dirs=', 'L', "directories to search for external C libraries"),
|
||||
('noisy', None, "show every action (compile, link, run, ...) taken"),
|
||||
(
|
||||
'dump-source',
|
||||
None,
|
||||
"dump generated source files before attempting to compile them",
|
||||
),
|
||||
]
|
||||
|
||||
# The three standard command methods: since the "config" command
|
||||
# does nothing by default, these are empty.
|
||||
|
||||
def initialize_options(self):
|
||||
self.compiler = None
|
||||
self.cc = None
|
||||
self.include_dirs = None
|
||||
self.libraries = None
|
||||
self.library_dirs = None
|
||||
|
||||
# maximal output for now
|
||||
self.noisy = 1
|
||||
self.dump_source = 1
|
||||
|
||||
# list of temporary files generated along-the-way that we have
|
||||
# to clean at some point
|
||||
self.temp_files = []
|
||||
|
||||
def finalize_options(self):
|
||||
if self.include_dirs is None:
|
||||
self.include_dirs = self.distribution.include_dirs or []
|
||||
elif isinstance(self.include_dirs, str):
|
||||
self.include_dirs = self.include_dirs.split(os.pathsep)
|
||||
|
||||
if self.libraries is None:
|
||||
self.libraries = []
|
||||
elif isinstance(self.libraries, str):
|
||||
self.libraries = [self.libraries]
|
||||
|
||||
if self.library_dirs is None:
|
||||
self.library_dirs = []
|
||||
elif isinstance(self.library_dirs, str):
|
||||
self.library_dirs = self.library_dirs.split(os.pathsep)
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
# Utility methods for actual "config" commands. The interfaces are
|
||||
# loosely based on Autoconf macros of similar names. Sub-classes
|
||||
# may use these freely.
|
||||
|
||||
def _check_compiler(self):
|
||||
"""Check that 'self.compiler' really is a CCompiler object;
|
||||
if not, make it one.
|
||||
"""
|
||||
if not isinstance(self.compiler, CCompiler):
|
||||
self.compiler = new_compiler(
|
||||
compiler=self.compiler, dry_run=self.dry_run, force=True
|
||||
)
|
||||
customize_compiler(self.compiler)
|
||||
if self.include_dirs:
|
||||
self.compiler.set_include_dirs(self.include_dirs)
|
||||
if self.libraries:
|
||||
self.compiler.set_libraries(self.libraries)
|
||||
if self.library_dirs:
|
||||
self.compiler.set_library_dirs(self.library_dirs)
|
||||
|
||||
def _gen_temp_sourcefile(self, body, headers, lang):
|
||||
filename = "_configtest" + LANG_EXT[lang]
|
||||
with open(filename, "w", encoding='utf-8') as file:
|
||||
if headers:
|
||||
for header in headers:
|
||||
file.write(f"#include <{header}>\n")
|
||||
file.write("\n")
|
||||
file.write(body)
|
||||
if body[-1] != "\n":
|
||||
file.write("\n")
|
||||
return filename
|
||||
|
||||
def _preprocess(self, body, headers, include_dirs, lang):
|
||||
src = self._gen_temp_sourcefile(body, headers, lang)
|
||||
out = "_configtest.i"
|
||||
self.temp_files.extend([src, out])
|
||||
self.compiler.preprocess(src, out, include_dirs=include_dirs)
|
||||
return (src, out)
|
||||
|
||||
def _compile(self, body, headers, include_dirs, lang):
|
||||
src = self._gen_temp_sourcefile(body, headers, lang)
|
||||
if self.dump_source:
|
||||
dump_file(src, f"compiling '{src}':")
|
||||
(obj,) = self.compiler.object_filenames([src])
|
||||
self.temp_files.extend([src, obj])
|
||||
self.compiler.compile([src], include_dirs=include_dirs)
|
||||
return (src, obj)
|
||||
|
||||
def _link(self, body, headers, include_dirs, libraries, library_dirs, lang):
|
||||
(src, obj) = self._compile(body, headers, include_dirs, lang)
|
||||
prog = os.path.splitext(os.path.basename(src))[0]
|
||||
self.compiler.link_executable(
|
||||
[obj],
|
||||
prog,
|
||||
libraries=libraries,
|
||||
library_dirs=library_dirs,
|
||||
target_lang=lang,
|
||||
)
|
||||
|
||||
if self.compiler.exe_extension is not None:
|
||||
prog = prog + self.compiler.exe_extension
|
||||
self.temp_files.append(prog)
|
||||
|
||||
return (src, obj, prog)
|
||||
|
||||
def _clean(self, *filenames):
|
||||
if not filenames:
|
||||
filenames = self.temp_files
|
||||
self.temp_files = []
|
||||
log.info("removing: %s", ' '.join(filenames))
|
||||
for filename in filenames:
|
||||
try:
|
||||
os.remove(filename)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# XXX these ignore the dry-run flag: what to do, what to do? even if
|
||||
# you want a dry-run build, you still need some sort of configuration
|
||||
# info. My inclination is to make it up to the real config command to
|
||||
# consult 'dry_run', and assume a default (minimal) configuration if
|
||||
# true. The problem with trying to do it here is that you'd have to
|
||||
# return either true or false from all the 'try' methods, neither of
|
||||
# which is correct.
|
||||
|
||||
# XXX need access to the header search path and maybe default macros.
|
||||
|
||||
def try_cpp(self, body=None, headers=None, include_dirs=None, lang="c"):
|
||||
"""Construct a source file from 'body' (a string containing lines
|
||||
of C/C++ code) and 'headers' (a list of header files to include)
|
||||
and run it through the preprocessor. Return true if the
|
||||
preprocessor succeeded, false if there were any errors.
|
||||
('body' probably isn't of much use, but what the heck.)
|
||||
"""
|
||||
self._check_compiler()
|
||||
ok = True
|
||||
try:
|
||||
self._preprocess(body, headers, include_dirs, lang)
|
||||
except CompileError:
|
||||
ok = False
|
||||
|
||||
self._clean()
|
||||
return ok
|
||||
|
||||
def search_cpp(self, pattern, body=None, headers=None, include_dirs=None, lang="c"):
|
||||
"""Construct a source file (just like 'try_cpp()'), run it through
|
||||
the preprocessor, and return true if any line of the output matches
|
||||
'pattern'. 'pattern' should either be a compiled regex object or a
|
||||
string containing a regex. If both 'body' and 'headers' are None,
|
||||
preprocesses an empty file -- which can be useful to determine the
|
||||
symbols the preprocessor and compiler set by default.
|
||||
"""
|
||||
self._check_compiler()
|
||||
src, out = self._preprocess(body, headers, include_dirs, lang)
|
||||
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
|
||||
with open(out, encoding='utf-8') as file:
|
||||
match = any(pattern.search(line) for line in file)
|
||||
|
||||
self._clean()
|
||||
return match
|
||||
|
||||
def try_compile(self, body, headers=None, include_dirs=None, lang="c"):
|
||||
"""Try to compile a source file built from 'body' and 'headers'.
|
||||
Return true on success, false otherwise.
|
||||
"""
|
||||
self._check_compiler()
|
||||
try:
|
||||
self._compile(body, headers, include_dirs, lang)
|
||||
ok = True
|
||||
except CompileError:
|
||||
ok = False
|
||||
|
||||
log.info(ok and "success!" or "failure.")
|
||||
self._clean()
|
||||
return ok
|
||||
|
||||
def try_link(
|
||||
self,
|
||||
body,
|
||||
headers=None,
|
||||
include_dirs=None,
|
||||
libraries=None,
|
||||
library_dirs=None,
|
||||
lang="c",
|
||||
):
|
||||
"""Try to compile and link a source file, built from 'body' and
|
||||
'headers', to executable form. Return true on success, false
|
||||
otherwise.
|
||||
"""
|
||||
self._check_compiler()
|
||||
try:
|
||||
self._link(body, headers, include_dirs, libraries, library_dirs, lang)
|
||||
ok = True
|
||||
except (CompileError, LinkError):
|
||||
ok = False
|
||||
|
||||
log.info(ok and "success!" or "failure.")
|
||||
self._clean()
|
||||
return ok
|
||||
|
||||
def try_run(
|
||||
self,
|
||||
body,
|
||||
headers=None,
|
||||
include_dirs=None,
|
||||
libraries=None,
|
||||
library_dirs=None,
|
||||
lang="c",
|
||||
):
|
||||
"""Try to compile, link to an executable, and run a program
|
||||
built from 'body' and 'headers'. Return true on success, false
|
||||
otherwise.
|
||||
"""
|
||||
self._check_compiler()
|
||||
try:
|
||||
src, obj, exe = self._link(
|
||||
body, headers, include_dirs, libraries, library_dirs, lang
|
||||
)
|
||||
self.spawn([exe])
|
||||
ok = True
|
||||
except (CompileError, LinkError, DistutilsExecError):
|
||||
ok = False
|
||||
|
||||
log.info(ok and "success!" or "failure.")
|
||||
self._clean()
|
||||
return ok
|
||||
|
||||
# -- High-level methods --------------------------------------------
|
||||
# (these are the ones that are actually likely to be useful
|
||||
# when implementing a real-world config command!)
|
||||
|
||||
def check_func(
|
||||
self,
|
||||
func,
|
||||
headers=None,
|
||||
include_dirs=None,
|
||||
libraries=None,
|
||||
library_dirs=None,
|
||||
decl=False,
|
||||
call=False,
|
||||
):
|
||||
"""Determine if function 'func' is available by constructing a
|
||||
source file that refers to 'func', and compiles and links it.
|
||||
If everything succeeds, returns true; otherwise returns false.
|
||||
|
||||
The constructed source file starts out by including the header
|
||||
files listed in 'headers'. If 'decl' is true, it then declares
|
||||
'func' (as "int func()"); you probably shouldn't supply 'headers'
|
||||
and set 'decl' true in the same call, or you might get errors about
|
||||
a conflicting declarations for 'func'. Finally, the constructed
|
||||
'main()' function either references 'func' or (if 'call' is true)
|
||||
calls it. 'libraries' and 'library_dirs' are used when
|
||||
linking.
|
||||
"""
|
||||
self._check_compiler()
|
||||
body = []
|
||||
if decl:
|
||||
body.append(f"int {func} ();")
|
||||
body.append("int main () {")
|
||||
if call:
|
||||
body.append(f" {func}();")
|
||||
else:
|
||||
body.append(f" {func};")
|
||||
body.append("}")
|
||||
body = "\n".join(body) + "\n"
|
||||
|
||||
return self.try_link(body, headers, include_dirs, libraries, library_dirs)
|
||||
|
||||
def check_lib(
|
||||
self,
|
||||
library,
|
||||
library_dirs=None,
|
||||
headers=None,
|
||||
include_dirs=None,
|
||||
other_libraries: Sequence[str] = [],
|
||||
):
|
||||
"""Determine if 'library' is available to be linked against,
|
||||
without actually checking that any particular symbols are provided
|
||||
by it. 'headers' will be used in constructing the source file to
|
||||
be compiled, but the only effect of this is to check if all the
|
||||
header files listed are available. Any libraries listed in
|
||||
'other_libraries' will be included in the link, in case 'library'
|
||||
has symbols that depend on other libraries.
|
||||
"""
|
||||
self._check_compiler()
|
||||
return self.try_link(
|
||||
"int main (void) { }",
|
||||
headers,
|
||||
include_dirs,
|
||||
[library] + list(other_libraries),
|
||||
library_dirs,
|
||||
)
|
||||
|
||||
def check_header(self, header, include_dirs=None, library_dirs=None, lang="c"):
|
||||
"""Determine if the system header file named by 'header_file'
|
||||
exists and can be found by the preprocessor; return true if so,
|
||||
false otherwise.
|
||||
"""
|
||||
return self.try_cpp(
|
||||
body="/* No body */", headers=[header], include_dirs=include_dirs
|
||||
)
|
||||
|
||||
|
||||
def dump_file(filename, head=None):
|
||||
"""Dumps a file content into log.info.
|
||||
|
||||
If head is not None, will be dumped before the file content.
|
||||
"""
|
||||
if head is None:
|
||||
log.info('%s', filename)
|
||||
else:
|
||||
log.info(head)
|
||||
log.info(pathlib.Path(filename).read_text(encoding='utf-8'))
|
||||
805
.venv/Lib/site-packages/setuptools/_distutils/command/install.py
Normal file
805
.venv/Lib/site-packages/setuptools/_distutils/command/install.py
Normal file
@@ -0,0 +1,805 @@
|
||||
"""distutils.command.install
|
||||
|
||||
Implements the Distutils 'install' command."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import itertools
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
from distutils._log import log
|
||||
from site import USER_BASE, USER_SITE
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..debug import DEBUG
|
||||
from ..errors import DistutilsOptionError, DistutilsPlatformError
|
||||
from ..file_util import write_file
|
||||
from ..sysconfig import get_config_vars
|
||||
from ..util import change_root, convert_path, get_platform, subst_vars
|
||||
from . import _framework_compat as fw
|
||||
|
||||
HAS_USER_SITE = True
|
||||
|
||||
WINDOWS_SCHEME = {
|
||||
'purelib': '{base}/Lib/site-packages',
|
||||
'platlib': '{base}/Lib/site-packages',
|
||||
'headers': '{base}/Include/{dist_name}',
|
||||
'scripts': '{base}/Scripts',
|
||||
'data': '{base}',
|
||||
}
|
||||
|
||||
INSTALL_SCHEMES = {
|
||||
'posix_prefix': {
|
||||
'purelib': '{base}/lib/{implementation_lower}{py_version_short}/site-packages',
|
||||
'platlib': '{platbase}/{platlibdir}/{implementation_lower}'
|
||||
'{py_version_short}/site-packages',
|
||||
'headers': '{base}/include/{implementation_lower}'
|
||||
'{py_version_short}{abiflags}/{dist_name}',
|
||||
'scripts': '{base}/bin',
|
||||
'data': '{base}',
|
||||
},
|
||||
'posix_home': {
|
||||
'purelib': '{base}/lib/{implementation_lower}',
|
||||
'platlib': '{base}/{platlibdir}/{implementation_lower}',
|
||||
'headers': '{base}/include/{implementation_lower}/{dist_name}',
|
||||
'scripts': '{base}/bin',
|
||||
'data': '{base}',
|
||||
},
|
||||
'nt': WINDOWS_SCHEME,
|
||||
'pypy': {
|
||||
'purelib': '{base}/site-packages',
|
||||
'platlib': '{base}/site-packages',
|
||||
'headers': '{base}/include/{dist_name}',
|
||||
'scripts': '{base}/bin',
|
||||
'data': '{base}',
|
||||
},
|
||||
'pypy_nt': {
|
||||
'purelib': '{base}/site-packages',
|
||||
'platlib': '{base}/site-packages',
|
||||
'headers': '{base}/include/{dist_name}',
|
||||
'scripts': '{base}/Scripts',
|
||||
'data': '{base}',
|
||||
},
|
||||
}
|
||||
|
||||
# user site schemes
|
||||
if HAS_USER_SITE:
|
||||
INSTALL_SCHEMES['nt_user'] = {
|
||||
'purelib': '{usersite}',
|
||||
'platlib': '{usersite}',
|
||||
'headers': '{userbase}/{implementation}{py_version_nodot_plat}'
|
||||
'/Include/{dist_name}',
|
||||
'scripts': '{userbase}/{implementation}{py_version_nodot_plat}/Scripts',
|
||||
'data': '{userbase}',
|
||||
}
|
||||
|
||||
INSTALL_SCHEMES['posix_user'] = {
|
||||
'purelib': '{usersite}',
|
||||
'platlib': '{usersite}',
|
||||
'headers': '{userbase}/include/{implementation_lower}'
|
||||
'{py_version_short}{abiflags}/{dist_name}',
|
||||
'scripts': '{userbase}/bin',
|
||||
'data': '{userbase}',
|
||||
}
|
||||
|
||||
|
||||
INSTALL_SCHEMES.update(fw.schemes)
|
||||
|
||||
|
||||
# The keys to an installation scheme; if any new types of files are to be
|
||||
# installed, be sure to add an entry to every installation scheme above,
|
||||
# and to SCHEME_KEYS here.
|
||||
SCHEME_KEYS = ('purelib', 'platlib', 'headers', 'scripts', 'data')
|
||||
|
||||
|
||||
def _load_sysconfig_schemes():
|
||||
with contextlib.suppress(AttributeError):
|
||||
return {
|
||||
scheme: sysconfig.get_paths(scheme, expand=False)
|
||||
for scheme in sysconfig.get_scheme_names()
|
||||
}
|
||||
|
||||
|
||||
def _load_schemes():
|
||||
"""
|
||||
Extend default schemes with schemes from sysconfig.
|
||||
"""
|
||||
|
||||
sysconfig_schemes = _load_sysconfig_schemes() or {}
|
||||
|
||||
return {
|
||||
scheme: {
|
||||
**INSTALL_SCHEMES.get(scheme, {}),
|
||||
**sysconfig_schemes.get(scheme, {}),
|
||||
}
|
||||
for scheme in set(itertools.chain(INSTALL_SCHEMES, sysconfig_schemes))
|
||||
}
|
||||
|
||||
|
||||
def _get_implementation():
|
||||
if hasattr(sys, 'pypy_version_info'):
|
||||
return 'PyPy'
|
||||
else:
|
||||
return 'Python'
|
||||
|
||||
|
||||
def _select_scheme(ob, name):
|
||||
scheme = _inject_headers(name, _load_scheme(_resolve_scheme(name)))
|
||||
vars(ob).update(_remove_set(ob, _scheme_attrs(scheme)))
|
||||
|
||||
|
||||
def _remove_set(ob, attrs):
|
||||
"""
|
||||
Include only attrs that are None in ob.
|
||||
"""
|
||||
return {key: value for key, value in attrs.items() if getattr(ob, key) is None}
|
||||
|
||||
|
||||
def _resolve_scheme(name):
|
||||
os_name, sep, key = name.partition('_')
|
||||
try:
|
||||
resolved = sysconfig.get_preferred_scheme(key)
|
||||
except Exception:
|
||||
resolved = fw.scheme(name)
|
||||
return resolved
|
||||
|
||||
|
||||
def _load_scheme(name):
|
||||
return _load_schemes()[name]
|
||||
|
||||
|
||||
def _inject_headers(name, scheme):
|
||||
"""
|
||||
Given a scheme name and the resolved scheme,
|
||||
if the scheme does not include headers, resolve
|
||||
the fallback scheme for the name and use headers
|
||||
from it. pypa/distutils#88
|
||||
"""
|
||||
# Bypass the preferred scheme, which may not
|
||||
# have defined headers.
|
||||
fallback = _load_scheme(name)
|
||||
scheme.setdefault('headers', fallback['headers'])
|
||||
return scheme
|
||||
|
||||
|
||||
def _scheme_attrs(scheme):
|
||||
"""Resolve install directories by applying the install schemes."""
|
||||
return {f'install_{key}': scheme[key] for key in SCHEME_KEYS}
|
||||
|
||||
|
||||
class install(Command):
|
||||
description = "install everything from build directory"
|
||||
|
||||
user_options = [
|
||||
# Select installation scheme and set base director(y|ies)
|
||||
('prefix=', None, "installation prefix"),
|
||||
('exec-prefix=', None, "(Unix only) prefix for platform-specific files"),
|
||||
('home=', None, "(Unix only) home directory to install under"),
|
||||
# Or, just set the base director(y|ies)
|
||||
(
|
||||
'install-base=',
|
||||
None,
|
||||
"base installation directory (instead of --prefix or --home)",
|
||||
),
|
||||
(
|
||||
'install-platbase=',
|
||||
None,
|
||||
"base installation directory for platform-specific files (instead of --exec-prefix or --home)",
|
||||
),
|
||||
('root=', None, "install everything relative to this alternate root directory"),
|
||||
# Or, explicitly set the installation scheme
|
||||
(
|
||||
'install-purelib=',
|
||||
None,
|
||||
"installation directory for pure Python module distributions",
|
||||
),
|
||||
(
|
||||
'install-platlib=',
|
||||
None,
|
||||
"installation directory for non-pure module distributions",
|
||||
),
|
||||
(
|
||||
'install-lib=',
|
||||
None,
|
||||
"installation directory for all module distributions (overrides --install-purelib and --install-platlib)",
|
||||
),
|
||||
('install-headers=', None, "installation directory for C/C++ headers"),
|
||||
('install-scripts=', None, "installation directory for Python scripts"),
|
||||
('install-data=', None, "installation directory for data files"),
|
||||
# Byte-compilation options -- see install_lib.py for details, as
|
||||
# these are duplicated from there (but only install_lib does
|
||||
# anything with them).
|
||||
('compile', 'c', "compile .py to .pyc [default]"),
|
||||
('no-compile', None, "don't compile .py files"),
|
||||
(
|
||||
'optimize=',
|
||||
'O',
|
||||
"also compile with optimization: -O1 for \"python -O\", "
|
||||
"-O2 for \"python -OO\", and -O0 to disable [default: -O0]",
|
||||
),
|
||||
# Miscellaneous control options
|
||||
('force', 'f', "force installation (overwrite any existing files)"),
|
||||
('skip-build', None, "skip rebuilding everything (for testing/debugging)"),
|
||||
# Where to install documentation (eventually!)
|
||||
# ('doc-format=', None, "format of documentation to generate"),
|
||||
# ('install-man=', None, "directory for Unix man pages"),
|
||||
# ('install-html=', None, "directory for HTML documentation"),
|
||||
# ('install-info=', None, "directory for GNU info files"),
|
||||
('record=', None, "filename in which to record list of installed files"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['compile', 'force', 'skip-build']
|
||||
|
||||
if HAS_USER_SITE:
|
||||
user_options.append((
|
||||
'user',
|
||||
None,
|
||||
f"install in user site-package '{USER_SITE}'",
|
||||
))
|
||||
boolean_options.append('user')
|
||||
|
||||
negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
|
||||
|
||||
def initialize_options(self) -> None:
|
||||
"""Initializes options."""
|
||||
# High-level options: these select both an installation base
|
||||
# and scheme.
|
||||
self.prefix: str | None = None
|
||||
self.exec_prefix: str | None = None
|
||||
self.home: str | None = None
|
||||
self.user = False
|
||||
|
||||
# These select only the installation base; it's up to the user to
|
||||
# specify the installation scheme (currently, that means supplying
|
||||
# the --install-{platlib,purelib,scripts,data} options).
|
||||
self.install_base = None
|
||||
self.install_platbase = None
|
||||
self.root: str | None = None
|
||||
|
||||
# These options are the actual installation directories; if not
|
||||
# supplied by the user, they are filled in using the installation
|
||||
# scheme implied by prefix/exec-prefix/home and the contents of
|
||||
# that installation scheme.
|
||||
self.install_purelib = None # for pure module distributions
|
||||
self.install_platlib = None # non-pure (dists w/ extensions)
|
||||
self.install_headers = None # for C/C++ headers
|
||||
self.install_lib: str | None = None # set to either purelib or platlib
|
||||
self.install_scripts = None
|
||||
self.install_data = None
|
||||
self.install_userbase = USER_BASE
|
||||
self.install_usersite = USER_SITE
|
||||
|
||||
self.compile = None
|
||||
self.optimize = None
|
||||
|
||||
# Deprecated
|
||||
# These two are for putting non-packagized distributions into their
|
||||
# own directory and creating a .pth file if it makes sense.
|
||||
# 'extra_path' comes from the setup file; 'install_path_file' can
|
||||
# be turned off if it makes no sense to install a .pth file. (But
|
||||
# better to install it uselessly than to guess wrong and not
|
||||
# install it when it's necessary and would be used!) Currently,
|
||||
# 'install_path_file' is always true unless some outsider meddles
|
||||
# with it.
|
||||
self.extra_path = None
|
||||
self.install_path_file = True
|
||||
|
||||
# 'force' forces installation, even if target files are not
|
||||
# out-of-date. 'skip_build' skips running the "build" command,
|
||||
# handy if you know it's not necessary. 'warn_dir' (which is *not*
|
||||
# a user option, it's just there so the bdist_* commands can turn
|
||||
# it off) determines whether we warn about installing to a
|
||||
# directory not in sys.path.
|
||||
self.force = False
|
||||
self.skip_build = False
|
||||
self.warn_dir = True
|
||||
|
||||
# These are only here as a conduit from the 'build' command to the
|
||||
# 'install_*' commands that do the real work. ('build_base' isn't
|
||||
# actually used anywhere, but it might be useful in future.) They
|
||||
# are not user options, because if the user told the install
|
||||
# command where the build directory is, that wouldn't affect the
|
||||
# build command.
|
||||
self.build_base = None
|
||||
self.build_lib = None
|
||||
|
||||
# Not defined yet because we don't know anything about
|
||||
# documentation yet.
|
||||
# self.install_man = None
|
||||
# self.install_html = None
|
||||
# self.install_info = None
|
||||
|
||||
self.record = None
|
||||
|
||||
# -- Option finalizing methods -------------------------------------
|
||||
# (This is rather more involved than for most commands,
|
||||
# because this is where the policy for installing third-
|
||||
# party Python modules on various platforms given a wide
|
||||
# array of user input is decided. Yes, it's quite complex!)
|
||||
|
||||
def finalize_options(self) -> None: # noqa: C901
|
||||
"""Finalizes options."""
|
||||
# This method (and its helpers, like 'finalize_unix()',
|
||||
# 'finalize_other()', and 'select_scheme()') is where the default
|
||||
# installation directories for modules, extension modules, and
|
||||
# anything else we care to install from a Python module
|
||||
# distribution. Thus, this code makes a pretty important policy
|
||||
# statement about how third-party stuff is added to a Python
|
||||
# installation! Note that the actual work of installation is done
|
||||
# by the relatively simple 'install_*' commands; they just take
|
||||
# their orders from the installation directory options determined
|
||||
# here.
|
||||
|
||||
# Check for errors/inconsistencies in the options; first, stuff
|
||||
# that's wrong on any platform.
|
||||
|
||||
if (self.prefix or self.exec_prefix or self.home) and (
|
||||
self.install_base or self.install_platbase
|
||||
):
|
||||
raise DistutilsOptionError(
|
||||
"must supply either prefix/exec-prefix/home or install-base/install-platbase -- not both"
|
||||
)
|
||||
|
||||
if self.home and (self.prefix or self.exec_prefix):
|
||||
raise DistutilsOptionError(
|
||||
"must supply either home or prefix/exec-prefix -- not both"
|
||||
)
|
||||
|
||||
if self.user and (
|
||||
self.prefix
|
||||
or self.exec_prefix
|
||||
or self.home
|
||||
or self.install_base
|
||||
or self.install_platbase
|
||||
):
|
||||
raise DistutilsOptionError(
|
||||
"can't combine user with prefix, "
|
||||
"exec_prefix/home, or install_(plat)base"
|
||||
)
|
||||
|
||||
# Next, stuff that's wrong (or dubious) only on certain platforms.
|
||||
if os.name != "posix":
|
||||
if self.exec_prefix:
|
||||
self.warn("exec-prefix option ignored on this platform")
|
||||
self.exec_prefix = None
|
||||
|
||||
# Now the interesting logic -- so interesting that we farm it out
|
||||
# to other methods. The goal of these methods is to set the final
|
||||
# values for the install_{lib,scripts,data,...} options, using as
|
||||
# input a heady brew of prefix, exec_prefix, home, install_base,
|
||||
# install_platbase, user-supplied versions of
|
||||
# install_{purelib,platlib,lib,scripts,data,...}, and the
|
||||
# install schemes. Phew!
|
||||
|
||||
self.dump_dirs("pre-finalize_{unix,other}")
|
||||
|
||||
if os.name == 'posix':
|
||||
self.finalize_unix()
|
||||
else:
|
||||
self.finalize_other()
|
||||
|
||||
self.dump_dirs("post-finalize_{unix,other}()")
|
||||
|
||||
# Expand configuration variables, tilde, etc. in self.install_base
|
||||
# and self.install_platbase -- that way, we can use $base or
|
||||
# $platbase in the other installation directories and not worry
|
||||
# about needing recursive variable expansion (shudder).
|
||||
|
||||
py_version = sys.version.split()[0]
|
||||
(prefix, exec_prefix) = get_config_vars('prefix', 'exec_prefix')
|
||||
try:
|
||||
abiflags = sys.abiflags
|
||||
except AttributeError:
|
||||
# sys.abiflags may not be defined on all platforms.
|
||||
abiflags = ''
|
||||
local_vars = {
|
||||
'dist_name': self.distribution.get_name(),
|
||||
'dist_version': self.distribution.get_version(),
|
||||
'dist_fullname': self.distribution.get_fullname(),
|
||||
'py_version': py_version,
|
||||
'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}',
|
||||
'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}',
|
||||
'sys_prefix': prefix,
|
||||
'prefix': prefix,
|
||||
'sys_exec_prefix': exec_prefix,
|
||||
'exec_prefix': exec_prefix,
|
||||
'abiflags': abiflags,
|
||||
'platlibdir': getattr(sys, 'platlibdir', 'lib'),
|
||||
'implementation_lower': _get_implementation().lower(),
|
||||
'implementation': _get_implementation(),
|
||||
}
|
||||
|
||||
# vars for compatibility on older Pythons
|
||||
compat_vars = dict(
|
||||
# Python 3.9 and earlier
|
||||
py_version_nodot_plat=getattr(sys, 'winver', '').replace('.', ''),
|
||||
)
|
||||
|
||||
if HAS_USER_SITE:
|
||||
local_vars['userbase'] = self.install_userbase
|
||||
local_vars['usersite'] = self.install_usersite
|
||||
|
||||
self.config_vars = collections.ChainMap(
|
||||
local_vars,
|
||||
sysconfig.get_config_vars(),
|
||||
compat_vars,
|
||||
fw.vars(),
|
||||
)
|
||||
|
||||
self.expand_basedirs()
|
||||
|
||||
self.dump_dirs("post-expand_basedirs()")
|
||||
|
||||
# Now define config vars for the base directories so we can expand
|
||||
# everything else.
|
||||
local_vars['base'] = self.install_base
|
||||
local_vars['platbase'] = self.install_platbase
|
||||
|
||||
if DEBUG:
|
||||
from pprint import pprint
|
||||
|
||||
print("config vars:")
|
||||
pprint(dict(self.config_vars))
|
||||
|
||||
# Expand "~" and configuration variables in the installation
|
||||
# directories.
|
||||
self.expand_dirs()
|
||||
|
||||
self.dump_dirs("post-expand_dirs()")
|
||||
|
||||
# Create directories in the home dir:
|
||||
if self.user:
|
||||
self.create_home_path()
|
||||
|
||||
# Pick the actual directory to install all modules to: either
|
||||
# install_purelib or install_platlib, depending on whether this
|
||||
# module distribution is pure or not. Of course, if the user
|
||||
# already specified install_lib, use their selection.
|
||||
if self.install_lib is None:
|
||||
if self.distribution.has_ext_modules(): # has extensions: non-pure
|
||||
self.install_lib = self.install_platlib
|
||||
else:
|
||||
self.install_lib = self.install_purelib
|
||||
|
||||
# Convert directories from Unix /-separated syntax to the local
|
||||
# convention.
|
||||
self.convert_paths(
|
||||
'lib',
|
||||
'purelib',
|
||||
'platlib',
|
||||
'scripts',
|
||||
'data',
|
||||
'headers',
|
||||
'userbase',
|
||||
'usersite',
|
||||
)
|
||||
|
||||
# Deprecated
|
||||
# Well, we're not actually fully completely finalized yet: we still
|
||||
# have to deal with 'extra_path', which is the hack for allowing
|
||||
# non-packagized module distributions (hello, Numerical Python!) to
|
||||
# get their own directories.
|
||||
self.handle_extra_path()
|
||||
self.install_libbase = self.install_lib # needed for .pth file
|
||||
self.install_lib = os.path.join(self.install_lib, self.extra_dirs)
|
||||
|
||||
# If a new root directory was supplied, make all the installation
|
||||
# dirs relative to it.
|
||||
if self.root is not None:
|
||||
self.change_roots(
|
||||
'libbase', 'lib', 'purelib', 'platlib', 'scripts', 'data', 'headers'
|
||||
)
|
||||
|
||||
self.dump_dirs("after prepending root")
|
||||
|
||||
# Find out the build directories, ie. where to install from.
|
||||
self.set_undefined_options(
|
||||
'build', ('build_base', 'build_base'), ('build_lib', 'build_lib')
|
||||
)
|
||||
|
||||
# Punt on doc directories for now -- after all, we're punting on
|
||||
# documentation completely!
|
||||
|
||||
def dump_dirs(self, msg) -> None:
|
||||
"""Dumps the list of user options."""
|
||||
if not DEBUG:
|
||||
return
|
||||
from ..fancy_getopt import longopt_xlate
|
||||
|
||||
log.debug(msg + ":")
|
||||
for opt in self.user_options:
|
||||
opt_name = opt[0]
|
||||
if opt_name[-1] == "=":
|
||||
opt_name = opt_name[0:-1]
|
||||
if opt_name in self.negative_opt:
|
||||
opt_name = self.negative_opt[opt_name]
|
||||
opt_name = opt_name.translate(longopt_xlate)
|
||||
val = not getattr(self, opt_name)
|
||||
else:
|
||||
opt_name = opt_name.translate(longopt_xlate)
|
||||
val = getattr(self, opt_name)
|
||||
log.debug(" %s: %s", opt_name, val)
|
||||
|
||||
def finalize_unix(self) -> None:
|
||||
"""Finalizes options for posix platforms."""
|
||||
if self.install_base is not None or self.install_platbase is not None:
|
||||
incomplete_scheme = (
|
||||
(
|
||||
self.install_lib is None
|
||||
and self.install_purelib is None
|
||||
and self.install_platlib is None
|
||||
)
|
||||
or self.install_headers is None
|
||||
or self.install_scripts is None
|
||||
or self.install_data is None
|
||||
)
|
||||
if incomplete_scheme:
|
||||
raise DistutilsOptionError(
|
||||
"install-base or install-platbase supplied, but "
|
||||
"installation scheme is incomplete"
|
||||
)
|
||||
return
|
||||
|
||||
if self.user:
|
||||
if self.install_userbase is None:
|
||||
raise DistutilsPlatformError("User base directory is not specified")
|
||||
self.install_base = self.install_platbase = self.install_userbase
|
||||
self.select_scheme("posix_user")
|
||||
elif self.home is not None:
|
||||
self.install_base = self.install_platbase = self.home
|
||||
self.select_scheme("posix_home")
|
||||
else:
|
||||
if self.prefix is None:
|
||||
if self.exec_prefix is not None:
|
||||
raise DistutilsOptionError(
|
||||
"must not supply exec-prefix without prefix"
|
||||
)
|
||||
|
||||
# Allow Fedora to add components to the prefix
|
||||
_prefix_addition = getattr(sysconfig, '_prefix_addition', "")
|
||||
|
||||
self.prefix = os.path.normpath(sys.prefix) + _prefix_addition
|
||||
self.exec_prefix = os.path.normpath(sys.exec_prefix) + _prefix_addition
|
||||
|
||||
else:
|
||||
if self.exec_prefix is None:
|
||||
self.exec_prefix = self.prefix
|
||||
|
||||
self.install_base = self.prefix
|
||||
self.install_platbase = self.exec_prefix
|
||||
self.select_scheme("posix_prefix")
|
||||
|
||||
def finalize_other(self) -> None:
|
||||
"""Finalizes options for non-posix platforms"""
|
||||
if self.user:
|
||||
if self.install_userbase is None:
|
||||
raise DistutilsPlatformError("User base directory is not specified")
|
||||
self.install_base = self.install_platbase = self.install_userbase
|
||||
self.select_scheme(os.name + "_user")
|
||||
elif self.home is not None:
|
||||
self.install_base = self.install_platbase = self.home
|
||||
self.select_scheme("posix_home")
|
||||
else:
|
||||
if self.prefix is None:
|
||||
self.prefix = os.path.normpath(sys.prefix)
|
||||
|
||||
self.install_base = self.install_platbase = self.prefix
|
||||
try:
|
||||
self.select_scheme(os.name)
|
||||
except KeyError:
|
||||
raise DistutilsPlatformError(
|
||||
f"I don't know how to install stuff on '{os.name}'"
|
||||
)
|
||||
|
||||
def select_scheme(self, name) -> None:
|
||||
_select_scheme(self, name)
|
||||
|
||||
def _expand_attrs(self, attrs):
|
||||
for attr in attrs:
|
||||
val = getattr(self, attr)
|
||||
if val is not None:
|
||||
if os.name in ('posix', 'nt'):
|
||||
val = os.path.expanduser(val)
|
||||
val = subst_vars(val, self.config_vars)
|
||||
setattr(self, attr, val)
|
||||
|
||||
def expand_basedirs(self) -> None:
|
||||
"""Calls `os.path.expanduser` on install_base, install_platbase and
|
||||
root."""
|
||||
self._expand_attrs(['install_base', 'install_platbase', 'root'])
|
||||
|
||||
def expand_dirs(self) -> None:
|
||||
"""Calls `os.path.expanduser` on install dirs."""
|
||||
self._expand_attrs([
|
||||
'install_purelib',
|
||||
'install_platlib',
|
||||
'install_lib',
|
||||
'install_headers',
|
||||
'install_scripts',
|
||||
'install_data',
|
||||
])
|
||||
|
||||
def convert_paths(self, *names) -> None:
|
||||
"""Call `convert_path` over `names`."""
|
||||
for name in names:
|
||||
attr = "install_" + name
|
||||
setattr(self, attr, convert_path(getattr(self, attr)))
|
||||
|
||||
def handle_extra_path(self) -> None:
|
||||
"""Set `path_file` and `extra_dirs` using `extra_path`."""
|
||||
if self.extra_path is None:
|
||||
self.extra_path = self.distribution.extra_path
|
||||
|
||||
if self.extra_path is not None:
|
||||
log.warning(
|
||||
"Distribution option extra_path is deprecated. "
|
||||
"See issue27919 for details."
|
||||
)
|
||||
if isinstance(self.extra_path, str):
|
||||
self.extra_path = self.extra_path.split(',')
|
||||
|
||||
if len(self.extra_path) == 1:
|
||||
path_file = extra_dirs = self.extra_path[0]
|
||||
elif len(self.extra_path) == 2:
|
||||
path_file, extra_dirs = self.extra_path
|
||||
else:
|
||||
raise DistutilsOptionError(
|
||||
"'extra_path' option must be a list, tuple, or "
|
||||
"comma-separated string with 1 or 2 elements"
|
||||
)
|
||||
|
||||
# convert to local form in case Unix notation used (as it
|
||||
# should be in setup scripts)
|
||||
extra_dirs = convert_path(extra_dirs)
|
||||
else:
|
||||
path_file = None
|
||||
extra_dirs = ''
|
||||
|
||||
# XXX should we warn if path_file and not extra_dirs? (in which
|
||||
# case the path file would be harmless but pointless)
|
||||
self.path_file = path_file
|
||||
self.extra_dirs = extra_dirs
|
||||
|
||||
def change_roots(self, *names) -> None:
|
||||
"""Change the install directories pointed by name using root."""
|
||||
for name in names:
|
||||
attr = "install_" + name
|
||||
setattr(self, attr, change_root(self.root, getattr(self, attr)))
|
||||
|
||||
def create_home_path(self) -> None:
|
||||
"""Create directories under ~."""
|
||||
if not self.user:
|
||||
return
|
||||
home = convert_path(os.path.expanduser("~"))
|
||||
for path in self.config_vars.values():
|
||||
if str(path).startswith(home) and not os.path.isdir(path):
|
||||
self.debug_print(f"os.makedirs('{path}', 0o700)")
|
||||
os.makedirs(path, 0o700)
|
||||
|
||||
# -- Command execution methods -------------------------------------
|
||||
|
||||
def run(self):
|
||||
"""Runs the command."""
|
||||
# Obviously have to build before we can install
|
||||
if not self.skip_build:
|
||||
self.run_command('build')
|
||||
# If we built for any other platform, we can't install.
|
||||
build_plat = self.distribution.get_command_obj('build').plat_name
|
||||
# check warn_dir - it is a clue that the 'install' is happening
|
||||
# internally, and not to sys.path, so we don't check the platform
|
||||
# matches what we are running.
|
||||
if self.warn_dir and build_plat != get_platform():
|
||||
raise DistutilsPlatformError("Can't install when cross-compiling")
|
||||
|
||||
# Run all sub-commands (at least those that need to be run)
|
||||
for cmd_name in self.get_sub_commands():
|
||||
self.run_command(cmd_name)
|
||||
|
||||
if self.path_file:
|
||||
self.create_path_file()
|
||||
|
||||
# write list of installed files, if requested.
|
||||
if self.record:
|
||||
outputs = self.get_outputs()
|
||||
if self.root: # strip any package prefix
|
||||
root_len = len(self.root)
|
||||
for counter in range(len(outputs)):
|
||||
outputs[counter] = outputs[counter][root_len:]
|
||||
self.execute(
|
||||
write_file,
|
||||
(self.record, outputs),
|
||||
f"writing list of installed files to '{self.record}'",
|
||||
)
|
||||
|
||||
sys_path = map(os.path.normpath, sys.path)
|
||||
sys_path = map(os.path.normcase, sys_path)
|
||||
install_lib = os.path.normcase(os.path.normpath(self.install_lib))
|
||||
if (
|
||||
self.warn_dir
|
||||
and not (self.path_file and self.install_path_file)
|
||||
and install_lib not in sys_path
|
||||
):
|
||||
log.debug(
|
||||
(
|
||||
"modules installed to '%s', which is not in "
|
||||
"Python's module search path (sys.path) -- "
|
||||
"you'll have to change the search path yourself"
|
||||
),
|
||||
self.install_lib,
|
||||
)
|
||||
|
||||
def create_path_file(self):
|
||||
"""Creates the .pth file"""
|
||||
filename = os.path.join(self.install_libbase, self.path_file + ".pth")
|
||||
if self.install_path_file:
|
||||
self.execute(
|
||||
write_file, (filename, [self.extra_dirs]), f"creating {filename}"
|
||||
)
|
||||
else:
|
||||
self.warn(f"path file '{filename}' not created")
|
||||
|
||||
# -- Reporting methods ---------------------------------------------
|
||||
|
||||
def get_outputs(self):
|
||||
"""Assembles the outputs of all the sub-commands."""
|
||||
outputs = []
|
||||
for cmd_name in self.get_sub_commands():
|
||||
cmd = self.get_finalized_command(cmd_name)
|
||||
# Add the contents of cmd.get_outputs(), ensuring
|
||||
# that outputs doesn't contain duplicate entries
|
||||
for filename in cmd.get_outputs():
|
||||
if filename not in outputs:
|
||||
outputs.append(filename)
|
||||
|
||||
if self.path_file and self.install_path_file:
|
||||
outputs.append(os.path.join(self.install_libbase, self.path_file + ".pth"))
|
||||
|
||||
return outputs
|
||||
|
||||
def get_inputs(self):
|
||||
"""Returns the inputs of all the sub-commands"""
|
||||
# XXX gee, this looks familiar ;-(
|
||||
inputs = []
|
||||
for cmd_name in self.get_sub_commands():
|
||||
cmd = self.get_finalized_command(cmd_name)
|
||||
inputs.extend(cmd.get_inputs())
|
||||
|
||||
return inputs
|
||||
|
||||
# -- Predicates for sub-command list -------------------------------
|
||||
|
||||
def has_lib(self):
|
||||
"""Returns true if the current distribution has any Python
|
||||
modules to install."""
|
||||
return (
|
||||
self.distribution.has_pure_modules() or self.distribution.has_ext_modules()
|
||||
)
|
||||
|
||||
def has_headers(self):
|
||||
"""Returns true if the current distribution has any headers to
|
||||
install."""
|
||||
return self.distribution.has_headers()
|
||||
|
||||
def has_scripts(self):
|
||||
"""Returns true if the current distribution has any scripts to.
|
||||
install."""
|
||||
return self.distribution.has_scripts()
|
||||
|
||||
def has_data(self):
|
||||
"""Returns true if the current distribution has any data to.
|
||||
install."""
|
||||
return self.distribution.has_data_files()
|
||||
|
||||
# 'sub_commands': a list of commands this command might have to run to
|
||||
# get its work done. See cmd.py for more info.
|
||||
sub_commands = [
|
||||
('install_lib', has_lib),
|
||||
('install_headers', has_headers),
|
||||
('install_scripts', has_scripts),
|
||||
('install_data', has_data),
|
||||
('install_egg_info', lambda self: True),
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
"""distutils.command.install_data
|
||||
|
||||
Implements the Distutils 'install_data' command, for installing
|
||||
platform-independent data files."""
|
||||
|
||||
# contributed by Bastian Kleineidam
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..util import change_root, convert_path
|
||||
|
||||
|
||||
class install_data(Command):
|
||||
description = "install data files"
|
||||
|
||||
user_options = [
|
||||
(
|
||||
'install-dir=',
|
||||
'd',
|
||||
"base directory for installing data files [default: installation base dir]",
|
||||
),
|
||||
('root=', None, "install everything relative to this alternate root directory"),
|
||||
('force', 'f', "force installation (overwrite existing files)"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['force']
|
||||
|
||||
def initialize_options(self):
|
||||
self.install_dir = None
|
||||
self.outfiles = []
|
||||
self.root = None
|
||||
self.force = False
|
||||
self.data_files = self.distribution.data_files
|
||||
self.warn_dir = True
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
self.set_undefined_options(
|
||||
'install',
|
||||
('install_data', 'install_dir'),
|
||||
('root', 'root'),
|
||||
('force', 'force'),
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
self.mkpath(self.install_dir)
|
||||
for f in self.data_files:
|
||||
self._copy(f)
|
||||
|
||||
@functools.singledispatchmethod
|
||||
def _copy(self, f: tuple[str | os.PathLike, Iterable[str | os.PathLike]]):
|
||||
# it's a tuple with path to install to and a list of files
|
||||
dir = convert_path(f[0])
|
||||
if not os.path.isabs(dir):
|
||||
dir = os.path.join(self.install_dir, dir)
|
||||
elif self.root:
|
||||
dir = change_root(self.root, dir)
|
||||
self.mkpath(dir)
|
||||
|
||||
if f[1] == []:
|
||||
# If there are no files listed, the user must be
|
||||
# trying to create an empty directory, so add the
|
||||
# directory to the list of output files.
|
||||
self.outfiles.append(dir)
|
||||
else:
|
||||
# Copy files, adding them to the list of output files.
|
||||
for data in f[1]:
|
||||
data = convert_path(data)
|
||||
(out, _) = self.copy_file(data, dir)
|
||||
self.outfiles.append(out)
|
||||
|
||||
@_copy.register(str)
|
||||
@_copy.register(os.PathLike)
|
||||
def _(self, f: str | os.PathLike):
|
||||
# it's a simple file, so copy it
|
||||
f = convert_path(f)
|
||||
if self.warn_dir:
|
||||
self.warn(
|
||||
"setup script did not provide a directory for "
|
||||
f"'{f}' -- installing right in '{self.install_dir}'"
|
||||
)
|
||||
(out, _) = self.copy_file(f, self.install_dir)
|
||||
self.outfiles.append(out)
|
||||
|
||||
def get_inputs(self):
|
||||
return self.data_files or []
|
||||
|
||||
def get_outputs(self):
|
||||
return self.outfiles
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
distutils.command.install_egg_info
|
||||
|
||||
Implements the Distutils 'install_egg_info' command, for installing
|
||||
a package's PKG-INFO metadata.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import ClassVar
|
||||
|
||||
from .. import dir_util
|
||||
from .._log import log
|
||||
from ..cmd import Command
|
||||
|
||||
|
||||
class install_egg_info(Command):
|
||||
"""Install an .egg-info file for the package"""
|
||||
|
||||
description = "Install package's PKG-INFO metadata as an .egg-info file"
|
||||
user_options: ClassVar[list[tuple[str, str, str]]] = [
|
||||
('install-dir=', 'd', "directory to install to"),
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
self.install_dir = None
|
||||
|
||||
@property
|
||||
def basename(self):
|
||||
"""
|
||||
Allow basename to be overridden by child class.
|
||||
Ref pypa/distutils#2.
|
||||
"""
|
||||
name = to_filename(safe_name(self.distribution.get_name()))
|
||||
version = to_filename(safe_version(self.distribution.get_version()))
|
||||
return f"{name}-{version}-py{sys.version_info.major}.{sys.version_info.minor}.egg-info"
|
||||
|
||||
def finalize_options(self):
|
||||
self.set_undefined_options('install_lib', ('install_dir', 'install_dir'))
|
||||
self.target = os.path.join(self.install_dir, self.basename)
|
||||
self.outputs = [self.target]
|
||||
|
||||
def run(self):
|
||||
target = self.target
|
||||
if os.path.isdir(target) and not os.path.islink(target):
|
||||
dir_util.remove_tree(target, dry_run=self.dry_run)
|
||||
elif os.path.exists(target):
|
||||
self.execute(os.unlink, (self.target,), "Removing " + target)
|
||||
elif not os.path.isdir(self.install_dir):
|
||||
self.execute(
|
||||
os.makedirs, (self.install_dir,), "Creating " + self.install_dir
|
||||
)
|
||||
log.info("Writing %s", target)
|
||||
if not self.dry_run:
|
||||
with open(target, 'w', encoding='UTF-8') as f:
|
||||
self.distribution.metadata.write_pkg_file(f)
|
||||
|
||||
def get_outputs(self):
|
||||
return self.outputs
|
||||
|
||||
|
||||
# The following routines are taken from setuptools' pkg_resources module and
|
||||
# can be replaced by importing them from pkg_resources once it is included
|
||||
# in the stdlib.
|
||||
|
||||
|
||||
def safe_name(name):
|
||||
"""Convert an arbitrary string to a standard distribution name
|
||||
|
||||
Any runs of non-alphanumeric/. characters are replaced with a single '-'.
|
||||
"""
|
||||
return re.sub('[^A-Za-z0-9.]+', '-', name)
|
||||
|
||||
|
||||
def safe_version(version):
|
||||
"""Convert an arbitrary string to a standard version string
|
||||
|
||||
Spaces become dots, and all other non-alphanumeric characters become
|
||||
dashes, with runs of multiple dashes condensed to a single dash.
|
||||
"""
|
||||
version = version.replace(' ', '.')
|
||||
return re.sub('[^A-Za-z0-9.]+', '-', version)
|
||||
|
||||
|
||||
def to_filename(name):
|
||||
"""Convert a project or version name to its filename-escaped form
|
||||
|
||||
Any '-' characters are currently replaced with '_'.
|
||||
"""
|
||||
return name.replace('-', '_')
|
||||
@@ -0,0 +1,46 @@
|
||||
"""distutils.command.install_headers
|
||||
|
||||
Implements the Distutils 'install_headers' command, to install C/C++ header
|
||||
files to the Python include directory."""
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
|
||||
|
||||
# XXX force is never used
|
||||
class install_headers(Command):
|
||||
description = "install C/C++ header files"
|
||||
|
||||
user_options: ClassVar[list[tuple[str, str, str]]] = [
|
||||
('install-dir=', 'd', "directory to install header files to"),
|
||||
('force', 'f', "force installation (overwrite existing files)"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['force']
|
||||
|
||||
def initialize_options(self):
|
||||
self.install_dir = None
|
||||
self.force = False
|
||||
self.outfiles = []
|
||||
|
||||
def finalize_options(self):
|
||||
self.set_undefined_options(
|
||||
'install', ('install_headers', 'install_dir'), ('force', 'force')
|
||||
)
|
||||
|
||||
def run(self):
|
||||
headers = self.distribution.headers
|
||||
if not headers:
|
||||
return
|
||||
|
||||
self.mkpath(self.install_dir)
|
||||
for header in headers:
|
||||
(out, _) = self.copy_file(header, self.install_dir)
|
||||
self.outfiles.append(out)
|
||||
|
||||
def get_inputs(self):
|
||||
return self.distribution.headers or []
|
||||
|
||||
def get_outputs(self):
|
||||
return self.outfiles
|
||||
@@ -0,0 +1,238 @@
|
||||
"""distutils.command.install_lib
|
||||
|
||||
Implements the Distutils 'install_lib' command
|
||||
(install all Python modules)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..errors import DistutilsOptionError
|
||||
|
||||
# Extension for Python source files.
|
||||
PYTHON_SOURCE_EXTENSION = ".py"
|
||||
|
||||
|
||||
class install_lib(Command):
|
||||
description = "install all Python modules (extensions and pure Python)"
|
||||
|
||||
# The byte-compilation options are a tad confusing. Here are the
|
||||
# possible scenarios:
|
||||
# 1) no compilation at all (--no-compile --no-optimize)
|
||||
# 2) compile .pyc only (--compile --no-optimize; default)
|
||||
# 3) compile .pyc and "opt-1" .pyc (--compile --optimize)
|
||||
# 4) compile "opt-1" .pyc only (--no-compile --optimize)
|
||||
# 5) compile .pyc and "opt-2" .pyc (--compile --optimize-more)
|
||||
# 6) compile "opt-2" .pyc only (--no-compile --optimize-more)
|
||||
#
|
||||
# The UI for this is two options, 'compile' and 'optimize'.
|
||||
# 'compile' is strictly boolean, and only decides whether to
|
||||
# generate .pyc files. 'optimize' is three-way (0, 1, or 2), and
|
||||
# decides both whether to generate .pyc files and what level of
|
||||
# optimization to use.
|
||||
|
||||
user_options = [
|
||||
('install-dir=', 'd', "directory to install to"),
|
||||
('build-dir=', 'b', "build directory (where to install from)"),
|
||||
('force', 'f', "force installation (overwrite existing files)"),
|
||||
('compile', 'c', "compile .py to .pyc [default]"),
|
||||
('no-compile', None, "don't compile .py files"),
|
||||
(
|
||||
'optimize=',
|
||||
'O',
|
||||
"also compile with optimization: -O1 for \"python -O\", "
|
||||
"-O2 for \"python -OO\", and -O0 to disable [default: -O0]",
|
||||
),
|
||||
('skip-build', None, "skip the build steps"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['force', 'compile', 'skip-build']
|
||||
negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
|
||||
|
||||
def initialize_options(self):
|
||||
# let the 'install' command dictate our installation directory
|
||||
self.install_dir = None
|
||||
self.build_dir = None
|
||||
self.force = False
|
||||
self.compile = None
|
||||
self.optimize = None
|
||||
self.skip_build = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
# Get all the information we need to install pure Python modules
|
||||
# from the umbrella 'install' command -- build (source) directory,
|
||||
# install (target) directory, and whether to compile .py files.
|
||||
self.set_undefined_options(
|
||||
'install',
|
||||
('build_lib', 'build_dir'),
|
||||
('install_lib', 'install_dir'),
|
||||
('force', 'force'),
|
||||
('compile', 'compile'),
|
||||
('optimize', 'optimize'),
|
||||
('skip_build', 'skip_build'),
|
||||
)
|
||||
|
||||
if self.compile is None:
|
||||
self.compile = True
|
||||
if self.optimize is None:
|
||||
self.optimize = False
|
||||
|
||||
if not isinstance(self.optimize, int):
|
||||
try:
|
||||
self.optimize = int(self.optimize)
|
||||
except ValueError:
|
||||
pass
|
||||
if self.optimize not in (0, 1, 2):
|
||||
raise DistutilsOptionError("optimize must be 0, 1, or 2")
|
||||
|
||||
def run(self) -> None:
|
||||
# Make sure we have built everything we need first
|
||||
self.build()
|
||||
|
||||
# Install everything: simply dump the entire contents of the build
|
||||
# directory to the installation directory (that's the beauty of
|
||||
# having a build directory!)
|
||||
outfiles = self.install()
|
||||
|
||||
# (Optionally) compile .py to .pyc
|
||||
if outfiles is not None and self.distribution.has_pure_modules():
|
||||
self.byte_compile(outfiles)
|
||||
|
||||
# -- Top-level worker functions ------------------------------------
|
||||
# (called from 'run()')
|
||||
|
||||
def build(self) -> None:
|
||||
if not self.skip_build:
|
||||
if self.distribution.has_pure_modules():
|
||||
self.run_command('build_py')
|
||||
if self.distribution.has_ext_modules():
|
||||
self.run_command('build_ext')
|
||||
|
||||
# Any: https://typing.readthedocs.io/en/latest/guides/writing_stubs.html#the-any-trick
|
||||
def install(self) -> list[str] | Any:
|
||||
if os.path.isdir(self.build_dir):
|
||||
outfiles = self.copy_tree(self.build_dir, self.install_dir)
|
||||
else:
|
||||
self.warn(
|
||||
f"'{self.build_dir}' does not exist -- no Python modules to install"
|
||||
)
|
||||
return
|
||||
return outfiles
|
||||
|
||||
def byte_compile(self, files) -> None:
|
||||
if sys.dont_write_bytecode:
|
||||
self.warn('byte-compiling is disabled, skipping.')
|
||||
return
|
||||
|
||||
from ..util import byte_compile
|
||||
|
||||
# Get the "--root" directory supplied to the "install" command,
|
||||
# and use it as a prefix to strip off the purported filename
|
||||
# encoded in bytecode files. This is far from complete, but it
|
||||
# should at least generate usable bytecode in RPM distributions.
|
||||
install_root = self.get_finalized_command('install').root
|
||||
|
||||
if self.compile:
|
||||
byte_compile(
|
||||
files,
|
||||
optimize=0,
|
||||
force=self.force,
|
||||
prefix=install_root,
|
||||
dry_run=self.dry_run,
|
||||
)
|
||||
if self.optimize > 0:
|
||||
byte_compile(
|
||||
files,
|
||||
optimize=self.optimize,
|
||||
force=self.force,
|
||||
prefix=install_root,
|
||||
verbose=self.verbose,
|
||||
dry_run=self.dry_run,
|
||||
)
|
||||
|
||||
# -- Utility methods -----------------------------------------------
|
||||
|
||||
def _mutate_outputs(self, has_any, build_cmd, cmd_option, output_dir):
|
||||
if not has_any:
|
||||
return []
|
||||
|
||||
build_cmd = self.get_finalized_command(build_cmd)
|
||||
build_files = build_cmd.get_outputs()
|
||||
build_dir = getattr(build_cmd, cmd_option)
|
||||
|
||||
prefix_len = len(build_dir) + len(os.sep)
|
||||
outputs = [os.path.join(output_dir, file[prefix_len:]) for file in build_files]
|
||||
|
||||
return outputs
|
||||
|
||||
def _bytecode_filenames(self, py_filenames):
|
||||
bytecode_files = []
|
||||
for py_file in py_filenames:
|
||||
# Since build_py handles package data installation, the
|
||||
# list of outputs can contain more than just .py files.
|
||||
# Make sure we only report bytecode for the .py files.
|
||||
ext = os.path.splitext(os.path.normcase(py_file))[1]
|
||||
if ext != PYTHON_SOURCE_EXTENSION:
|
||||
continue
|
||||
if self.compile:
|
||||
bytecode_files.append(
|
||||
importlib.util.cache_from_source(py_file, optimization='')
|
||||
)
|
||||
if self.optimize > 0:
|
||||
bytecode_files.append(
|
||||
importlib.util.cache_from_source(
|
||||
py_file, optimization=self.optimize
|
||||
)
|
||||
)
|
||||
|
||||
return bytecode_files
|
||||
|
||||
# -- External interface --------------------------------------------
|
||||
# (called by outsiders)
|
||||
|
||||
def get_outputs(self):
|
||||
"""Return the list of files that would be installed if this command
|
||||
were actually run. Not affected by the "dry-run" flag or whether
|
||||
modules have actually been built yet.
|
||||
"""
|
||||
pure_outputs = self._mutate_outputs(
|
||||
self.distribution.has_pure_modules(),
|
||||
'build_py',
|
||||
'build_lib',
|
||||
self.install_dir,
|
||||
)
|
||||
if self.compile:
|
||||
bytecode_outputs = self._bytecode_filenames(pure_outputs)
|
||||
else:
|
||||
bytecode_outputs = []
|
||||
|
||||
ext_outputs = self._mutate_outputs(
|
||||
self.distribution.has_ext_modules(),
|
||||
'build_ext',
|
||||
'build_lib',
|
||||
self.install_dir,
|
||||
)
|
||||
|
||||
return pure_outputs + bytecode_outputs + ext_outputs
|
||||
|
||||
def get_inputs(self):
|
||||
"""Get the list of files that are input to this command, ie. the
|
||||
files that get installed as they are named in the build tree.
|
||||
The files in this list correspond one-to-one to the output
|
||||
filenames returned by 'get_outputs()'.
|
||||
"""
|
||||
inputs = []
|
||||
|
||||
if self.distribution.has_pure_modules():
|
||||
build_py = self.get_finalized_command('build_py')
|
||||
inputs.extend(build_py.get_outputs())
|
||||
|
||||
if self.distribution.has_ext_modules():
|
||||
build_ext = self.get_finalized_command('build_ext')
|
||||
inputs.extend(build_ext.get_outputs())
|
||||
|
||||
return inputs
|
||||
@@ -0,0 +1,62 @@
|
||||
"""distutils.command.install_scripts
|
||||
|
||||
Implements the Distutils 'install_scripts' command, for installing
|
||||
Python scripts."""
|
||||
|
||||
# contributed by Bastian Kleineidam
|
||||
|
||||
import os
|
||||
from distutils._log import log
|
||||
from stat import ST_MODE
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
|
||||
|
||||
class install_scripts(Command):
|
||||
description = "install scripts (Python or otherwise)"
|
||||
|
||||
user_options = [
|
||||
('install-dir=', 'd', "directory to install scripts to"),
|
||||
('build-dir=', 'b', "build directory (where to install from)"),
|
||||
('force', 'f', "force installation (overwrite existing files)"),
|
||||
('skip-build', None, "skip the build steps"),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = ['force', 'skip-build']
|
||||
|
||||
def initialize_options(self):
|
||||
self.install_dir = None
|
||||
self.force = False
|
||||
self.build_dir = None
|
||||
self.skip_build = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
self.set_undefined_options('build', ('build_scripts', 'build_dir'))
|
||||
self.set_undefined_options(
|
||||
'install',
|
||||
('install_scripts', 'install_dir'),
|
||||
('force', 'force'),
|
||||
('skip_build', 'skip_build'),
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
if not self.skip_build:
|
||||
self.run_command('build_scripts')
|
||||
self.outfiles = self.copy_tree(self.build_dir, self.install_dir)
|
||||
if os.name == 'posix':
|
||||
# Set the executable bits (owner, group, and world) on
|
||||
# all the scripts we just installed.
|
||||
for file in self.get_outputs():
|
||||
if self.dry_run:
|
||||
log.info("changing mode of %s", file)
|
||||
else:
|
||||
mode = ((os.stat(file)[ST_MODE]) | 0o555) & 0o7777
|
||||
log.info("changing mode of %s to %o", file, mode)
|
||||
os.chmod(file, mode)
|
||||
|
||||
def get_inputs(self):
|
||||
return self.distribution.scripts or []
|
||||
|
||||
def get_outputs(self):
|
||||
return self.outfiles or []
|
||||
521
.venv/Lib/site-packages/setuptools/_distutils/command/sdist.py
Normal file
521
.venv/Lib/site-packages/setuptools/_distutils/command/sdist.py
Normal file
@@ -0,0 +1,521 @@
|
||||
"""distutils.command.sdist
|
||||
|
||||
Implements the Distutils 'sdist' command (create a source distribution)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from distutils import archive_util, dir_util, file_util
|
||||
from distutils._log import log
|
||||
from glob import glob
|
||||
from itertools import filterfalse
|
||||
from typing import ClassVar
|
||||
|
||||
from ..core import Command
|
||||
from ..errors import DistutilsOptionError, DistutilsTemplateError
|
||||
from ..filelist import FileList
|
||||
from ..text_file import TextFile
|
||||
from ..util import convert_path
|
||||
|
||||
|
||||
def show_formats():
|
||||
"""Print all possible values for the 'formats' option (used by
|
||||
the "--help-formats" command-line option).
|
||||
"""
|
||||
from ..archive_util import ARCHIVE_FORMATS
|
||||
from ..fancy_getopt import FancyGetopt
|
||||
|
||||
formats = sorted(
|
||||
("formats=" + format, None, ARCHIVE_FORMATS[format][2])
|
||||
for format in ARCHIVE_FORMATS.keys()
|
||||
)
|
||||
FancyGetopt(formats).print_help("List of available source distribution formats:")
|
||||
|
||||
|
||||
class sdist(Command):
|
||||
description = "create a source distribution (tarball, zip file, etc.)"
|
||||
|
||||
def checking_metadata(self) -> bool:
|
||||
"""Callable used for the check sub-command.
|
||||
|
||||
Placed here so user_options can view it"""
|
||||
return self.metadata_check
|
||||
|
||||
user_options = [
|
||||
('template=', 't', "name of manifest template file [default: MANIFEST.in]"),
|
||||
('manifest=', 'm', "name of manifest file [default: MANIFEST]"),
|
||||
(
|
||||
'use-defaults',
|
||||
None,
|
||||
"include the default file set in the manifest "
|
||||
"[default; disable with --no-defaults]",
|
||||
),
|
||||
('no-defaults', None, "don't include the default file set"),
|
||||
(
|
||||
'prune',
|
||||
None,
|
||||
"specifically exclude files/directories that should not be "
|
||||
"distributed (build tree, RCS/CVS dirs, etc.) "
|
||||
"[default; disable with --no-prune]",
|
||||
),
|
||||
('no-prune', None, "don't automatically exclude anything"),
|
||||
(
|
||||
'manifest-only',
|
||||
'o',
|
||||
"just regenerate the manifest and then stop (implies --force-manifest)",
|
||||
),
|
||||
(
|
||||
'force-manifest',
|
||||
'f',
|
||||
"forcibly regenerate the manifest and carry on as usual. "
|
||||
"Deprecated: now the manifest is always regenerated.",
|
||||
),
|
||||
('formats=', None, "formats for source distribution (comma-separated list)"),
|
||||
(
|
||||
'keep-temp',
|
||||
'k',
|
||||
"keep the distribution tree around after creating " + "archive file(s)",
|
||||
),
|
||||
(
|
||||
'dist-dir=',
|
||||
'd',
|
||||
"directory to put the source distribution archive(s) in [default: dist]",
|
||||
),
|
||||
(
|
||||
'metadata-check',
|
||||
None,
|
||||
"Ensure that all required elements of meta-data "
|
||||
"are supplied. Warn if any missing. [default]",
|
||||
),
|
||||
(
|
||||
'owner=',
|
||||
'u',
|
||||
"Owner name used when creating a tar file [default: current user]",
|
||||
),
|
||||
(
|
||||
'group=',
|
||||
'g',
|
||||
"Group name used when creating a tar file [default: current group]",
|
||||
),
|
||||
]
|
||||
|
||||
boolean_options: ClassVar[list[str]] = [
|
||||
'use-defaults',
|
||||
'prune',
|
||||
'manifest-only',
|
||||
'force-manifest',
|
||||
'keep-temp',
|
||||
'metadata-check',
|
||||
]
|
||||
|
||||
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
|
||||
('help-formats', None, "list available distribution formats", show_formats),
|
||||
]
|
||||
|
||||
negative_opt: ClassVar[dict[str, str]] = {
|
||||
'no-defaults': 'use-defaults',
|
||||
'no-prune': 'prune',
|
||||
}
|
||||
|
||||
sub_commands = [('check', checking_metadata)]
|
||||
|
||||
READMES: ClassVar[tuple[str, ...]] = ('README', 'README.txt', 'README.rst')
|
||||
|
||||
def initialize_options(self):
|
||||
# 'template' and 'manifest' are, respectively, the names of
|
||||
# the manifest template and manifest file.
|
||||
self.template = None
|
||||
self.manifest = None
|
||||
|
||||
# 'use_defaults': if true, we will include the default file set
|
||||
# in the manifest
|
||||
self.use_defaults = True
|
||||
self.prune = True
|
||||
|
||||
self.manifest_only = False
|
||||
self.force_manifest = False
|
||||
|
||||
self.formats = ['gztar']
|
||||
self.keep_temp = False
|
||||
self.dist_dir = None
|
||||
|
||||
self.archive_files = None
|
||||
self.metadata_check = True
|
||||
self.owner = None
|
||||
self.group = None
|
||||
|
||||
def finalize_options(self) -> None:
|
||||
if self.manifest is None:
|
||||
self.manifest = "MANIFEST"
|
||||
if self.template is None:
|
||||
self.template = "MANIFEST.in"
|
||||
|
||||
self.ensure_string_list('formats')
|
||||
|
||||
bad_format = archive_util.check_archive_formats(self.formats)
|
||||
if bad_format:
|
||||
raise DistutilsOptionError(f"unknown archive format '{bad_format}'")
|
||||
|
||||
if self.dist_dir is None:
|
||||
self.dist_dir = "dist"
|
||||
|
||||
def run(self) -> None:
|
||||
# 'filelist' contains the list of files that will make up the
|
||||
# manifest
|
||||
self.filelist = FileList()
|
||||
|
||||
# Run sub commands
|
||||
for cmd_name in self.get_sub_commands():
|
||||
self.run_command(cmd_name)
|
||||
|
||||
# Do whatever it takes to get the list of files to process
|
||||
# (process the manifest template, read an existing manifest,
|
||||
# whatever). File list is accumulated in 'self.filelist'.
|
||||
self.get_file_list()
|
||||
|
||||
# If user just wanted us to regenerate the manifest, stop now.
|
||||
if self.manifest_only:
|
||||
return
|
||||
|
||||
# Otherwise, go ahead and create the source distribution tarball,
|
||||
# or zipfile, or whatever.
|
||||
self.make_distribution()
|
||||
|
||||
def get_file_list(self) -> None:
|
||||
"""Figure out the list of files to include in the source
|
||||
distribution, and put it in 'self.filelist'. This might involve
|
||||
reading the manifest template (and writing the manifest), or just
|
||||
reading the manifest, or just using the default file set -- it all
|
||||
depends on the user's options.
|
||||
"""
|
||||
# new behavior when using a template:
|
||||
# the file list is recalculated every time because
|
||||
# even if MANIFEST.in or setup.py are not changed
|
||||
# the user might have added some files in the tree that
|
||||
# need to be included.
|
||||
#
|
||||
# This makes --force the default and only behavior with templates.
|
||||
template_exists = os.path.isfile(self.template)
|
||||
if not template_exists and self._manifest_is_not_generated():
|
||||
self.read_manifest()
|
||||
self.filelist.sort()
|
||||
self.filelist.remove_duplicates()
|
||||
return
|
||||
|
||||
if not template_exists:
|
||||
self.warn(
|
||||
("manifest template '%s' does not exist " + "(using default file list)")
|
||||
% self.template
|
||||
)
|
||||
self.filelist.findall()
|
||||
|
||||
if self.use_defaults:
|
||||
self.add_defaults()
|
||||
|
||||
if template_exists:
|
||||
self.read_template()
|
||||
|
||||
if self.prune:
|
||||
self.prune_file_list()
|
||||
|
||||
self.filelist.sort()
|
||||
self.filelist.remove_duplicates()
|
||||
self.write_manifest()
|
||||
|
||||
def add_defaults(self) -> None:
|
||||
"""Add all the default files to self.filelist:
|
||||
- README or README.txt
|
||||
- setup.py
|
||||
- tests/test*.py and test/test*.py
|
||||
- all pure Python modules mentioned in setup script
|
||||
- all files pointed by package_data (build_py)
|
||||
- all files defined in data_files.
|
||||
- all files defined as scripts.
|
||||
- all C sources listed as part of extensions or C libraries
|
||||
in the setup script (doesn't catch C headers!)
|
||||
Warns if (README or README.txt) or setup.py are missing; everything
|
||||
else is optional.
|
||||
"""
|
||||
self._add_defaults_standards()
|
||||
self._add_defaults_optional()
|
||||
self._add_defaults_python()
|
||||
self._add_defaults_data_files()
|
||||
self._add_defaults_ext()
|
||||
self._add_defaults_c_libs()
|
||||
self._add_defaults_scripts()
|
||||
|
||||
@staticmethod
|
||||
def _cs_path_exists(fspath):
|
||||
"""
|
||||
Case-sensitive path existence check
|
||||
|
||||
>>> sdist._cs_path_exists(__file__)
|
||||
True
|
||||
>>> sdist._cs_path_exists(__file__.upper())
|
||||
False
|
||||
"""
|
||||
if not os.path.exists(fspath):
|
||||
return False
|
||||
# make absolute so we always have a directory
|
||||
abspath = os.path.abspath(fspath)
|
||||
directory, filename = os.path.split(abspath)
|
||||
return filename in os.listdir(directory)
|
||||
|
||||
def _add_defaults_standards(self):
|
||||
standards = [self.READMES, self.distribution.script_name]
|
||||
for fn in standards:
|
||||
if isinstance(fn, tuple):
|
||||
alts = fn
|
||||
got_it = False
|
||||
for fn in alts:
|
||||
if self._cs_path_exists(fn):
|
||||
got_it = True
|
||||
self.filelist.append(fn)
|
||||
break
|
||||
|
||||
if not got_it:
|
||||
self.warn(
|
||||
"standard file not found: should have one of " + ', '.join(alts)
|
||||
)
|
||||
else:
|
||||
if self._cs_path_exists(fn):
|
||||
self.filelist.append(fn)
|
||||
else:
|
||||
self.warn(f"standard file '{fn}' not found")
|
||||
|
||||
def _add_defaults_optional(self):
|
||||
optional = ['tests/test*.py', 'test/test*.py', 'setup.cfg']
|
||||
for pattern in optional:
|
||||
files = filter(os.path.isfile, glob(pattern))
|
||||
self.filelist.extend(files)
|
||||
|
||||
def _add_defaults_python(self):
|
||||
# build_py is used to get:
|
||||
# - python modules
|
||||
# - files defined in package_data
|
||||
build_py = self.get_finalized_command('build_py')
|
||||
|
||||
# getting python files
|
||||
if self.distribution.has_pure_modules():
|
||||
self.filelist.extend(build_py.get_source_files())
|
||||
|
||||
# getting package_data files
|
||||
# (computed in build_py.data_files by build_py.finalize_options)
|
||||
for _pkg, src_dir, _build_dir, filenames in build_py.data_files:
|
||||
for filename in filenames:
|
||||
self.filelist.append(os.path.join(src_dir, filename))
|
||||
|
||||
def _add_defaults_data_files(self):
|
||||
# getting distribution.data_files
|
||||
if self.distribution.has_data_files():
|
||||
for item in self.distribution.data_files:
|
||||
if isinstance(item, str):
|
||||
# plain file
|
||||
item = convert_path(item)
|
||||
if os.path.isfile(item):
|
||||
self.filelist.append(item)
|
||||
else:
|
||||
# a (dirname, filenames) tuple
|
||||
dirname, filenames = item
|
||||
for f in filenames:
|
||||
f = convert_path(f)
|
||||
if os.path.isfile(f):
|
||||
self.filelist.append(f)
|
||||
|
||||
def _add_defaults_ext(self):
|
||||
if self.distribution.has_ext_modules():
|
||||
build_ext = self.get_finalized_command('build_ext')
|
||||
self.filelist.extend(build_ext.get_source_files())
|
||||
|
||||
def _add_defaults_c_libs(self):
|
||||
if self.distribution.has_c_libraries():
|
||||
build_clib = self.get_finalized_command('build_clib')
|
||||
self.filelist.extend(build_clib.get_source_files())
|
||||
|
||||
def _add_defaults_scripts(self):
|
||||
if self.distribution.has_scripts():
|
||||
build_scripts = self.get_finalized_command('build_scripts')
|
||||
self.filelist.extend(build_scripts.get_source_files())
|
||||
|
||||
def read_template(self) -> None:
|
||||
"""Read and parse manifest template file named by self.template.
|
||||
|
||||
(usually "MANIFEST.in") The parsing and processing is done by
|
||||
'self.filelist', which updates itself accordingly.
|
||||
"""
|
||||
log.info("reading manifest template '%s'", self.template)
|
||||
template = TextFile(
|
||||
self.template,
|
||||
strip_comments=True,
|
||||
skip_blanks=True,
|
||||
join_lines=True,
|
||||
lstrip_ws=True,
|
||||
rstrip_ws=True,
|
||||
collapse_join=True,
|
||||
)
|
||||
|
||||
try:
|
||||
while True:
|
||||
line = template.readline()
|
||||
if line is None: # end of file
|
||||
break
|
||||
|
||||
try:
|
||||
self.filelist.process_template_line(line)
|
||||
# the call above can raise a DistutilsTemplateError for
|
||||
# malformed lines, or a ValueError from the lower-level
|
||||
# convert_path function
|
||||
except (DistutilsTemplateError, ValueError) as msg:
|
||||
self.warn(
|
||||
f"{template.filename}, line {int(template.current_line)}: {msg}"
|
||||
)
|
||||
finally:
|
||||
template.close()
|
||||
|
||||
def prune_file_list(self) -> None:
|
||||
"""Prune off branches that might slip into the file list as created
|
||||
by 'read_template()', but really don't belong there:
|
||||
* the build tree (typically "build")
|
||||
* the release tree itself (only an issue if we ran "sdist"
|
||||
previously with --keep-temp, or it aborted)
|
||||
* any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories
|
||||
"""
|
||||
build = self.get_finalized_command('build')
|
||||
base_dir = self.distribution.get_fullname()
|
||||
|
||||
self.filelist.exclude_pattern(None, prefix=os.fspath(build.build_base))
|
||||
self.filelist.exclude_pattern(None, prefix=base_dir)
|
||||
|
||||
if sys.platform == 'win32':
|
||||
seps = r'/|\\'
|
||||
else:
|
||||
seps = '/'
|
||||
|
||||
vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', '_darcs']
|
||||
vcs_ptrn = r'(^|{})({})({}).*'.format(seps, '|'.join(vcs_dirs), seps)
|
||||
self.filelist.exclude_pattern(vcs_ptrn, is_regex=True)
|
||||
|
||||
def write_manifest(self) -> None:
|
||||
"""Write the file list in 'self.filelist' (presumably as filled in
|
||||
by 'add_defaults()' and 'read_template()') to the manifest file
|
||||
named by 'self.manifest'.
|
||||
"""
|
||||
if self._manifest_is_not_generated():
|
||||
log.info(
|
||||
f"not writing to manually maintained manifest file '{self.manifest}'"
|
||||
)
|
||||
return
|
||||
|
||||
content = self.filelist.files[:]
|
||||
content.insert(0, '# file GENERATED by distutils, do NOT edit')
|
||||
self.execute(
|
||||
file_util.write_file,
|
||||
(self.manifest, content),
|
||||
f"writing manifest file '{self.manifest}'",
|
||||
)
|
||||
|
||||
def _manifest_is_not_generated(self):
|
||||
# check for special comment used in 3.1.3 and higher
|
||||
if not os.path.isfile(self.manifest):
|
||||
return False
|
||||
|
||||
with open(self.manifest, encoding='utf-8') as fp:
|
||||
first_line = next(fp)
|
||||
return first_line != '# file GENERATED by distutils, do NOT edit\n'
|
||||
|
||||
def read_manifest(self) -> None:
|
||||
"""Read the manifest file (named by 'self.manifest') and use it to
|
||||
fill in 'self.filelist', the list of files to include in the source
|
||||
distribution.
|
||||
"""
|
||||
log.info("reading manifest file '%s'", self.manifest)
|
||||
with open(self.manifest, encoding='utf-8') as lines:
|
||||
self.filelist.extend(
|
||||
# ignore comments and blank lines
|
||||
filter(None, filterfalse(is_comment, map(str.strip, lines)))
|
||||
)
|
||||
|
||||
def make_release_tree(self, base_dir, files) -> None:
|
||||
"""Create the directory tree that will become the source
|
||||
distribution archive. All directories implied by the filenames in
|
||||
'files' are created under 'base_dir', and then we hard link or copy
|
||||
(if hard linking is unavailable) those files into place.
|
||||
Essentially, this duplicates the developer's source tree, but in a
|
||||
directory named after the distribution, containing only the files
|
||||
to be distributed.
|
||||
"""
|
||||
# Create all the directories under 'base_dir' necessary to
|
||||
# put 'files' there; the 'mkpath()' is just so we don't die
|
||||
# if the manifest happens to be empty.
|
||||
self.mkpath(base_dir)
|
||||
dir_util.create_tree(base_dir, files, dry_run=self.dry_run)
|
||||
|
||||
# And walk over the list of files, either making a hard link (if
|
||||
# os.link exists) to each one that doesn't already exist in its
|
||||
# corresponding location under 'base_dir', or copying each file
|
||||
# that's out-of-date in 'base_dir'. (Usually, all files will be
|
||||
# out-of-date, because by default we blow away 'base_dir' when
|
||||
# we're done making the distribution archives.)
|
||||
|
||||
if hasattr(os, 'link'): # can make hard links on this system
|
||||
link = 'hard'
|
||||
msg = f"making hard links in {base_dir}..."
|
||||
else: # nope, have to copy
|
||||
link = None
|
||||
msg = f"copying files to {base_dir}..."
|
||||
|
||||
if not files:
|
||||
log.warning("no files to distribute -- empty manifest?")
|
||||
else:
|
||||
log.info(msg)
|
||||
for file in files:
|
||||
if not os.path.isfile(file):
|
||||
log.warning("'%s' not a regular file -- skipping", file)
|
||||
else:
|
||||
dest = os.path.join(base_dir, file)
|
||||
self.copy_file(file, dest, link=link)
|
||||
|
||||
self.distribution.metadata.write_pkg_info(base_dir)
|
||||
|
||||
def make_distribution(self) -> None:
|
||||
"""Create the source distribution(s). First, we create the release
|
||||
tree with 'make_release_tree()'; then, we create all required
|
||||
archive files (according to 'self.formats') from the release tree.
|
||||
Finally, we clean up by blowing away the release tree (unless
|
||||
'self.keep_temp' is true). The list of archive files created is
|
||||
stored so it can be retrieved later by 'get_archive_files()'.
|
||||
"""
|
||||
# Don't warn about missing meta-data here -- should be (and is!)
|
||||
# done elsewhere.
|
||||
base_dir = self.distribution.get_fullname()
|
||||
base_name = os.path.join(self.dist_dir, base_dir)
|
||||
|
||||
self.make_release_tree(base_dir, self.filelist.files)
|
||||
archive_files = [] # remember names of files we create
|
||||
# tar archive must be created last to avoid overwrite and remove
|
||||
if 'tar' in self.formats:
|
||||
self.formats.append(self.formats.pop(self.formats.index('tar')))
|
||||
|
||||
for fmt in self.formats:
|
||||
file = self.make_archive(
|
||||
base_name, fmt, base_dir=base_dir, owner=self.owner, group=self.group
|
||||
)
|
||||
archive_files.append(file)
|
||||
self.distribution.dist_files.append(('sdist', '', file))
|
||||
|
||||
self.archive_files = archive_files
|
||||
|
||||
if not self.keep_temp:
|
||||
dir_util.remove_tree(base_dir, dry_run=self.dry_run)
|
||||
|
||||
def get_archive_files(self):
|
||||
"""Return the list of archive files created when the command
|
||||
was run, or None if the command hasn't run yet.
|
||||
"""
|
||||
return self.archive_files
|
||||
|
||||
|
||||
def is_comment(line: str) -> bool:
|
||||
return line.startswith('#')
|
||||
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TypeVar
|
||||
|
||||
_IterableT = TypeVar("_IterableT", bound="Iterable[str]")
|
||||
|
||||
|
||||
def consolidate_linker_args(args: _IterableT) -> _IterableT | str:
|
||||
"""
|
||||
Ensure the return value is a string for backward compatibility.
|
||||
|
||||
Retain until at least 2025-04-31. See pypa/distutils#246
|
||||
"""
|
||||
|
||||
if not all(arg.startswith('-Wl,') for arg in args):
|
||||
return args
|
||||
return '-Wl,' + ','.join(arg.removeprefix('-Wl,') for arg in args)
|
||||
@@ -0,0 +1,2 @@
|
||||
# required for older numpy versions on Pythons prior to 3.12; see pypa/setuptools#4876
|
||||
from ..compilers.C.base import _default_compilers, compiler_class # noqa: F401
|
||||
66
.venv/Lib/site-packages/setuptools/_distutils/compat/py39.py
Normal file
66
.venv/Lib/site-packages/setuptools/_distutils/compat/py39.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import functools
|
||||
import itertools
|
||||
import platform
|
||||
import sys
|
||||
|
||||
|
||||
def add_ext_suffix_39(vars):
|
||||
"""
|
||||
Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130
|
||||
"""
|
||||
import _imp
|
||||
|
||||
ext_suffix = _imp.extension_suffixes()[0]
|
||||
vars.update(
|
||||
EXT_SUFFIX=ext_suffix,
|
||||
# sysconfig sets SO to match EXT_SUFFIX, so maintain
|
||||
# that expectation.
|
||||
# https://github.com/python/cpython/blob/785cc6770588de087d09e89a69110af2542be208/Lib/sysconfig.py#L671-L673
|
||||
SO=ext_suffix,
|
||||
)
|
||||
|
||||
|
||||
needs_ext_suffix = sys.version_info < (3, 10) and platform.system() == 'Windows'
|
||||
add_ext_suffix = add_ext_suffix_39 if needs_ext_suffix else lambda vars: None
|
||||
|
||||
|
||||
# from more_itertools
|
||||
class UnequalIterablesError(ValueError):
|
||||
def __init__(self, details=None):
|
||||
msg = 'Iterables have different lengths'
|
||||
if details is not None:
|
||||
msg += (': index 0 has length {}; index {} has length {}').format(*details)
|
||||
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
# from more_itertools
|
||||
def _zip_equal_generator(iterables):
|
||||
_marker = object()
|
||||
for combo in itertools.zip_longest(*iterables, fillvalue=_marker):
|
||||
for val in combo:
|
||||
if val is _marker:
|
||||
raise UnequalIterablesError()
|
||||
yield combo
|
||||
|
||||
|
||||
# from more_itertools
|
||||
def _zip_equal(*iterables):
|
||||
# Check whether the iterables are all the same size.
|
||||
try:
|
||||
first_size = len(iterables[0])
|
||||
for i, it in enumerate(iterables[1:], 1):
|
||||
size = len(it)
|
||||
if size != first_size:
|
||||
raise UnequalIterablesError(details=(first_size, i, size))
|
||||
# All sizes are equal, we can use the built-in zip.
|
||||
return zip(*iterables)
|
||||
# If any one of the iterables didn't have a length, start reading
|
||||
# them until one runs out.
|
||||
except TypeError:
|
||||
return _zip_equal_generator(iterables)
|
||||
|
||||
|
||||
zip_strict = (
|
||||
_zip_equal if sys.version_info < (3, 10) else functools.partial(zip, strict=True)
|
||||
)
|
||||
1394
.venv/Lib/site-packages/setuptools/_distutils/compilers/C/base.py
Normal file
1394
.venv/Lib/site-packages/setuptools/_distutils/compilers/C/base.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,340 @@
|
||||
"""distutils.cygwinccompiler
|
||||
|
||||
Provides the CygwinCCompiler class, a subclass of UnixCCompiler that
|
||||
handles the Cygwin port of the GNU C compiler to Windows. It also contains
|
||||
the Mingw32CCompiler class which handles the mingw32 port of GCC (same as
|
||||
cygwin in no-cygwin mode).
|
||||
"""
|
||||
|
||||
import copy
|
||||
import os
|
||||
import pathlib
|
||||
import shlex
|
||||
import sys
|
||||
import warnings
|
||||
from subprocess import check_output
|
||||
|
||||
from ...errors import (
|
||||
DistutilsExecError,
|
||||
DistutilsPlatformError,
|
||||
)
|
||||
from ...file_util import write_file
|
||||
from ...sysconfig import get_config_vars
|
||||
from ...version import LooseVersion, suppress_known_deprecation
|
||||
from . import unix
|
||||
from .errors import (
|
||||
CompileError,
|
||||
Error,
|
||||
)
|
||||
|
||||
|
||||
def get_msvcr():
|
||||
"""No longer needed, but kept for backward compatibility."""
|
||||
return []
|
||||
|
||||
|
||||
_runtime_library_dirs_msg = (
|
||||
"Unable to set runtime library search path on Windows, "
|
||||
"usually indicated by `runtime_library_dirs` parameter to Extension"
|
||||
)
|
||||
|
||||
|
||||
class Compiler(unix.Compiler):
|
||||
"""Handles the Cygwin port of the GNU C compiler to Windows."""
|
||||
|
||||
compiler_type = 'cygwin'
|
||||
obj_extension = ".o"
|
||||
static_lib_extension = ".a"
|
||||
shared_lib_extension = ".dll.a"
|
||||
dylib_lib_extension = ".dll"
|
||||
static_lib_format = "lib%s%s"
|
||||
shared_lib_format = "lib%s%s"
|
||||
dylib_lib_format = "cyg%s%s"
|
||||
exe_extension = ".exe"
|
||||
|
||||
def __init__(self, verbose=False, dry_run=False, force=False):
|
||||
super().__init__(verbose, dry_run, force)
|
||||
|
||||
status, details = check_config_h()
|
||||
self.debug_print(f"Python's GCC status: {status} (details: {details})")
|
||||
if status is not CONFIG_H_OK:
|
||||
self.warn(
|
||||
"Python's pyconfig.h doesn't seem to support your compiler. "
|
||||
f"Reason: {details}. "
|
||||
"Compiling may fail because of undefined preprocessor macros."
|
||||
)
|
||||
|
||||
self.cc, self.cxx = get_config_vars('CC', 'CXX')
|
||||
|
||||
# Override 'CC' and 'CXX' environment variables for
|
||||
# building using MINGW compiler for MSVC python.
|
||||
self.cc = os.environ.get('CC', self.cc or 'gcc')
|
||||
self.cxx = os.environ.get('CXX', self.cxx or 'g++')
|
||||
|
||||
self.linker_dll = self.cc
|
||||
self.linker_dll_cxx = self.cxx
|
||||
shared_option = "-shared"
|
||||
|
||||
self.set_executables(
|
||||
compiler=f'{self.cc} -mcygwin -O -Wall',
|
||||
compiler_so=f'{self.cc} -mcygwin -mdll -O -Wall',
|
||||
compiler_cxx=f'{self.cxx} -mcygwin -O -Wall',
|
||||
compiler_so_cxx=f'{self.cxx} -mcygwin -mdll -O -Wall',
|
||||
linker_exe=f'{self.cc} -mcygwin',
|
||||
linker_so=f'{self.linker_dll} -mcygwin {shared_option}',
|
||||
linker_exe_cxx=f'{self.cxx} -mcygwin',
|
||||
linker_so_cxx=f'{self.linker_dll_cxx} -mcygwin {shared_option}',
|
||||
)
|
||||
|
||||
self.dll_libraries = get_msvcr()
|
||||
|
||||
@property
|
||||
def gcc_version(self):
|
||||
# Older numpy depended on this existing to check for ancient
|
||||
# gcc versions. This doesn't make much sense with clang etc so
|
||||
# just hardcode to something recent.
|
||||
# https://github.com/numpy/numpy/pull/20333
|
||||
warnings.warn(
|
||||
"gcc_version attribute of CygwinCCompiler is deprecated. "
|
||||
"Instead of returning actual gcc version a fixed value 11.2.0 is returned.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
with suppress_known_deprecation():
|
||||
return LooseVersion("11.2.0")
|
||||
|
||||
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
|
||||
"""Compiles the source by spawning GCC and windres if needed."""
|
||||
if ext in ('.rc', '.res'):
|
||||
# gcc needs '.res' and '.rc' compiled to object files !!!
|
||||
try:
|
||||
self.spawn(["windres", "-i", src, "-o", obj])
|
||||
except DistutilsExecError as msg:
|
||||
raise CompileError(msg)
|
||||
else: # for other files use the C-compiler
|
||||
try:
|
||||
if self.detect_language(src) == 'c++':
|
||||
self.spawn(
|
||||
self.compiler_so_cxx
|
||||
+ cc_args
|
||||
+ [src, '-o', obj]
|
||||
+ extra_postargs
|
||||
)
|
||||
else:
|
||||
self.spawn(
|
||||
self.compiler_so + cc_args + [src, '-o', obj] + extra_postargs
|
||||
)
|
||||
except DistutilsExecError as msg:
|
||||
raise CompileError(msg)
|
||||
|
||||
def link(
|
||||
self,
|
||||
target_desc,
|
||||
objects,
|
||||
output_filename,
|
||||
output_dir=None,
|
||||
libraries=None,
|
||||
library_dirs=None,
|
||||
runtime_library_dirs=None,
|
||||
export_symbols=None,
|
||||
debug=False,
|
||||
extra_preargs=None,
|
||||
extra_postargs=None,
|
||||
build_temp=None,
|
||||
target_lang=None,
|
||||
):
|
||||
"""Link the objects."""
|
||||
# use separate copies, so we can modify the lists
|
||||
extra_preargs = copy.copy(extra_preargs or [])
|
||||
libraries = copy.copy(libraries or [])
|
||||
objects = copy.copy(objects or [])
|
||||
|
||||
if runtime_library_dirs:
|
||||
self.warn(_runtime_library_dirs_msg)
|
||||
|
||||
# Additional libraries
|
||||
libraries.extend(self.dll_libraries)
|
||||
|
||||
# handle export symbols by creating a def-file
|
||||
# with executables this only works with gcc/ld as linker
|
||||
if (export_symbols is not None) and (
|
||||
target_desc != self.EXECUTABLE or self.linker_dll == "gcc"
|
||||
):
|
||||
# (The linker doesn't do anything if output is up-to-date.
|
||||
# So it would probably better to check if we really need this,
|
||||
# but for this we had to insert some unchanged parts of
|
||||
# UnixCCompiler, and this is not what we want.)
|
||||
|
||||
# we want to put some files in the same directory as the
|
||||
# object files are, build_temp doesn't help much
|
||||
# where are the object files
|
||||
temp_dir = os.path.dirname(objects[0])
|
||||
# name of dll to give the helper files the same base name
|
||||
(dll_name, dll_extension) = os.path.splitext(
|
||||
os.path.basename(output_filename)
|
||||
)
|
||||
|
||||
# generate the filenames for these files
|
||||
def_file = os.path.join(temp_dir, dll_name + ".def")
|
||||
|
||||
# Generate .def file
|
||||
contents = [f"LIBRARY {os.path.basename(output_filename)}", "EXPORTS"]
|
||||
contents.extend(export_symbols)
|
||||
self.execute(write_file, (def_file, contents), f"writing {def_file}")
|
||||
|
||||
# next add options for def-file
|
||||
|
||||
# for gcc/ld the def-file is specified as any object files
|
||||
objects.append(def_file)
|
||||
|
||||
# end: if ((export_symbols is not None) and
|
||||
# (target_desc != self.EXECUTABLE or self.linker_dll == "gcc")):
|
||||
|
||||
# who wants symbols and a many times larger output file
|
||||
# should explicitly switch the debug mode on
|
||||
# otherwise we let ld strip the output file
|
||||
# (On my machine: 10KiB < stripped_file < ??100KiB
|
||||
# unstripped_file = stripped_file + XXX KiB
|
||||
# ( XXX=254 for a typical python extension))
|
||||
if not debug:
|
||||
extra_preargs.append("-s")
|
||||
|
||||
super().link(
|
||||
target_desc,
|
||||
objects,
|
||||
output_filename,
|
||||
output_dir,
|
||||
libraries,
|
||||
library_dirs,
|
||||
runtime_library_dirs,
|
||||
None, # export_symbols, we do this in our def-file
|
||||
debug,
|
||||
extra_preargs,
|
||||
extra_postargs,
|
||||
build_temp,
|
||||
target_lang,
|
||||
)
|
||||
|
||||
def runtime_library_dir_option(self, dir):
|
||||
# cygwin doesn't support rpath. While in theory we could error
|
||||
# out like MSVC does, code might expect it to work like on Unix, so
|
||||
# just warn and hope for the best.
|
||||
self.warn(_runtime_library_dirs_msg)
|
||||
return []
|
||||
|
||||
# -- Miscellaneous methods -----------------------------------------
|
||||
|
||||
def _make_out_path(self, output_dir, strip_dir, src_name):
|
||||
# use normcase to make sure '.rc' is really '.rc' and not '.RC'
|
||||
norm_src_name = os.path.normcase(src_name)
|
||||
return super()._make_out_path(output_dir, strip_dir, norm_src_name)
|
||||
|
||||
@property
|
||||
def out_extensions(self):
|
||||
"""
|
||||
Add support for rc and res files.
|
||||
"""
|
||||
return {
|
||||
**super().out_extensions,
|
||||
**{ext: ext + self.obj_extension for ext in ('.res', '.rc')},
|
||||
}
|
||||
|
||||
|
||||
# the same as cygwin plus some additional parameters
|
||||
class MinGW32Compiler(Compiler):
|
||||
"""Handles the Mingw32 port of the GNU C compiler to Windows."""
|
||||
|
||||
compiler_type = 'mingw32'
|
||||
|
||||
def __init__(self, verbose=False, dry_run=False, force=False):
|
||||
super().__init__(verbose, dry_run, force)
|
||||
|
||||
shared_option = "-shared"
|
||||
|
||||
if is_cygwincc(self.cc):
|
||||
raise Error('Cygwin gcc cannot be used with --compiler=mingw32')
|
||||
|
||||
self.set_executables(
|
||||
compiler=f'{self.cc} -O -Wall',
|
||||
compiler_so=f'{self.cc} -shared -O -Wall',
|
||||
compiler_so_cxx=f'{self.cxx} -shared -O -Wall',
|
||||
compiler_cxx=f'{self.cxx} -O -Wall',
|
||||
linker_exe=f'{self.cc}',
|
||||
linker_so=f'{self.linker_dll} {shared_option}',
|
||||
linker_exe_cxx=f'{self.cxx}',
|
||||
linker_so_cxx=f'{self.linker_dll_cxx} {shared_option}',
|
||||
)
|
||||
|
||||
def runtime_library_dir_option(self, dir):
|
||||
raise DistutilsPlatformError(_runtime_library_dirs_msg)
|
||||
|
||||
|
||||
# Because these compilers aren't configured in Python's pyconfig.h file by
|
||||
# default, we should at least warn the user if he is using an unmodified
|
||||
# version.
|
||||
|
||||
CONFIG_H_OK = "ok"
|
||||
CONFIG_H_NOTOK = "not ok"
|
||||
CONFIG_H_UNCERTAIN = "uncertain"
|
||||
|
||||
|
||||
def check_config_h():
|
||||
"""Check if the current Python installation appears amenable to building
|
||||
extensions with GCC.
|
||||
|
||||
Returns a tuple (status, details), where 'status' is one of the following
|
||||
constants:
|
||||
|
||||
- CONFIG_H_OK: all is well, go ahead and compile
|
||||
- CONFIG_H_NOTOK: doesn't look good
|
||||
- CONFIG_H_UNCERTAIN: not sure -- unable to read pyconfig.h
|
||||
|
||||
'details' is a human-readable string explaining the situation.
|
||||
|
||||
Note there are two ways to conclude "OK": either 'sys.version' contains
|
||||
the string "GCC" (implying that this Python was built with GCC), or the
|
||||
installed "pyconfig.h" contains the string "__GNUC__".
|
||||
"""
|
||||
|
||||
# XXX since this function also checks sys.version, it's not strictly a
|
||||
# "pyconfig.h" check -- should probably be renamed...
|
||||
|
||||
from distutils import sysconfig
|
||||
|
||||
# if sys.version contains GCC then python was compiled with GCC, and the
|
||||
# pyconfig.h file should be OK
|
||||
if "GCC" in sys.version:
|
||||
return CONFIG_H_OK, "sys.version mentions 'GCC'"
|
||||
|
||||
# Clang would also work
|
||||
if "Clang" in sys.version:
|
||||
return CONFIG_H_OK, "sys.version mentions 'Clang'"
|
||||
|
||||
# let's see if __GNUC__ is mentioned in python.h
|
||||
fn = sysconfig.get_config_h_filename()
|
||||
try:
|
||||
config_h = pathlib.Path(fn).read_text(encoding='utf-8')
|
||||
except OSError as exc:
|
||||
return (CONFIG_H_UNCERTAIN, f"couldn't read '{fn}': {exc.strerror}")
|
||||
else:
|
||||
substring = '__GNUC__'
|
||||
if substring in config_h:
|
||||
code = CONFIG_H_OK
|
||||
mention_inflected = 'mentions'
|
||||
else:
|
||||
code = CONFIG_H_NOTOK
|
||||
mention_inflected = 'does not mention'
|
||||
return code, f"{fn!r} {mention_inflected} {substring!r}"
|
||||
|
||||
|
||||
def is_cygwincc(cc):
|
||||
"""Try to determine if the compiler that would be used is from cygwin."""
|
||||
out_string = check_output(shlex.split(cc) + ['-dumpmachine'])
|
||||
return out_string.strip().endswith(b'cygwin')
|
||||
|
||||
|
||||
get_versions = None
|
||||
"""
|
||||
A stand-in for the previous get_versions() function to prevent failures
|
||||
when monkeypatched. See pypa/setuptools#2969.
|
||||
"""
|
||||
@@ -0,0 +1,24 @@
|
||||
class Error(Exception):
|
||||
"""Some compile/link operation failed."""
|
||||
|
||||
|
||||
class PreprocessError(Error):
|
||||
"""Failure to preprocess one or more C/C++ files."""
|
||||
|
||||
|
||||
class CompileError(Error):
|
||||
"""Failure to compile one or more C/C++ source files."""
|
||||
|
||||
|
||||
class LibError(Error):
|
||||
"""Failure to create a static library from one or more C/C++ object
|
||||
files."""
|
||||
|
||||
|
||||
class LinkError(Error):
|
||||
"""Failure to link one or more C/C++ object files into an executable
|
||||
or shared library file."""
|
||||
|
||||
|
||||
class UnknownFileType(Error):
|
||||
"""Attempt to process an unknown file type."""
|
||||
@@ -0,0 +1,614 @@
|
||||
"""distutils._msvccompiler
|
||||
|
||||
Contains MSVCCompiler, an implementation of the abstract CCompiler class
|
||||
for Microsoft Visual Studio 2015.
|
||||
|
||||
This module requires VS 2015 or later.
|
||||
"""
|
||||
|
||||
# Written by Perry Stoll
|
||||
# hacked by Robin Becker and Thomas Heller to do a better job of
|
||||
# finding DevStudio (through the registry)
|
||||
# ported to VS 2005 and VS 2008 by Christian Heimes
|
||||
# ported to VS 2015 by Steve Dower
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import subprocess
|
||||
import unittest.mock as mock
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
import winreg
|
||||
|
||||
from itertools import count
|
||||
|
||||
from ..._log import log
|
||||
from ...errors import (
|
||||
DistutilsExecError,
|
||||
DistutilsPlatformError,
|
||||
)
|
||||
from ...util import get_host_platform, get_platform
|
||||
from . import base
|
||||
from .base import gen_lib_options
|
||||
from .errors import (
|
||||
CompileError,
|
||||
LibError,
|
||||
LinkError,
|
||||
)
|
||||
|
||||
|
||||
def _find_vc2015():
|
||||
try:
|
||||
key = winreg.OpenKeyEx(
|
||||
winreg.HKEY_LOCAL_MACHINE,
|
||||
r"Software\Microsoft\VisualStudio\SxS\VC7",
|
||||
access=winreg.KEY_READ | winreg.KEY_WOW64_32KEY,
|
||||
)
|
||||
except OSError:
|
||||
log.debug("Visual C++ is not registered")
|
||||
return None, None
|
||||
|
||||
best_version = 0
|
||||
best_dir = None
|
||||
with key:
|
||||
for i in count():
|
||||
try:
|
||||
v, vc_dir, vt = winreg.EnumValue(key, i)
|
||||
except OSError:
|
||||
break
|
||||
if v and vt == winreg.REG_SZ and os.path.isdir(vc_dir):
|
||||
try:
|
||||
version = int(float(v))
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if version >= 14 and version > best_version:
|
||||
best_version, best_dir = version, vc_dir
|
||||
return best_version, best_dir
|
||||
|
||||
|
||||
def _find_vc2017():
|
||||
"""Returns "15, path" based on the result of invoking vswhere.exe
|
||||
If no install is found, returns "None, None"
|
||||
|
||||
The version is returned to avoid unnecessarily changing the function
|
||||
result. It may be ignored when the path is not None.
|
||||
|
||||
If vswhere.exe is not available, by definition, VS 2017 is not
|
||||
installed.
|
||||
"""
|
||||
root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
|
||||
if not root:
|
||||
return None, None
|
||||
|
||||
variant = 'arm64' if get_platform() == 'win-arm64' else 'x86.x64'
|
||||
suitable_components = (
|
||||
f"Microsoft.VisualStudio.Component.VC.Tools.{variant}",
|
||||
"Microsoft.VisualStudio.Workload.WDExpress",
|
||||
)
|
||||
|
||||
for component in suitable_components:
|
||||
# Workaround for `-requiresAny` (only available on VS 2017 > 15.6)
|
||||
with contextlib.suppress(
|
||||
subprocess.CalledProcessError, OSError, UnicodeDecodeError
|
||||
):
|
||||
path = (
|
||||
subprocess.check_output([
|
||||
os.path.join(
|
||||
root, "Microsoft Visual Studio", "Installer", "vswhere.exe"
|
||||
),
|
||||
"-latest",
|
||||
"-prerelease",
|
||||
"-requires",
|
||||
component,
|
||||
"-property",
|
||||
"installationPath",
|
||||
"-products",
|
||||
"*",
|
||||
])
|
||||
.decode(encoding="mbcs", errors="strict")
|
||||
.strip()
|
||||
)
|
||||
|
||||
path = os.path.join(path, "VC", "Auxiliary", "Build")
|
||||
if os.path.isdir(path):
|
||||
return 15, path
|
||||
|
||||
return None, None # no suitable component found
|
||||
|
||||
|
||||
PLAT_SPEC_TO_RUNTIME = {
|
||||
'x86': 'x86',
|
||||
'x86_amd64': 'x64',
|
||||
'x86_arm': 'arm',
|
||||
'x86_arm64': 'arm64',
|
||||
}
|
||||
|
||||
|
||||
def _find_vcvarsall(plat_spec):
|
||||
# bpo-38597: Removed vcruntime return value
|
||||
_, best_dir = _find_vc2017()
|
||||
|
||||
if not best_dir:
|
||||
best_version, best_dir = _find_vc2015()
|
||||
|
||||
if not best_dir:
|
||||
log.debug("No suitable Visual C++ version found")
|
||||
return None, None
|
||||
|
||||
vcvarsall = os.path.join(best_dir, "vcvarsall.bat")
|
||||
if not os.path.isfile(vcvarsall):
|
||||
log.debug("%s cannot be found", vcvarsall)
|
||||
return None, None
|
||||
|
||||
return vcvarsall, None
|
||||
|
||||
|
||||
def _get_vc_env(plat_spec):
|
||||
if os.getenv("DISTUTILS_USE_SDK"):
|
||||
return {key.lower(): value for key, value in os.environ.items()}
|
||||
|
||||
vcvarsall, _ = _find_vcvarsall(plat_spec)
|
||||
if not vcvarsall:
|
||||
raise DistutilsPlatformError(
|
||||
'Microsoft Visual C++ 14.0 or greater is required. '
|
||||
'Get it with "Microsoft C++ Build Tools": '
|
||||
'https://visualstudio.microsoft.com/visual-cpp-build-tools/'
|
||||
)
|
||||
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
f'cmd /u /c "{vcvarsall}" {plat_spec} && set',
|
||||
stderr=subprocess.STDOUT,
|
||||
).decode('utf-16le', errors='replace')
|
||||
except subprocess.CalledProcessError as exc:
|
||||
log.error(exc.output)
|
||||
raise DistutilsPlatformError(f"Error executing {exc.cmd}")
|
||||
|
||||
env = {
|
||||
key.lower(): value
|
||||
for key, _, value in (line.partition('=') for line in out.splitlines())
|
||||
if key and value
|
||||
}
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def _find_exe(exe, paths=None):
|
||||
"""Return path to an MSVC executable program.
|
||||
|
||||
Tries to find the program in several places: first, one of the
|
||||
MSVC program search paths from the registry; next, the directories
|
||||
in the PATH environment variable. If any of those work, return an
|
||||
absolute path that is known to exist. If none of them work, just
|
||||
return the original program name, 'exe'.
|
||||
"""
|
||||
if not paths:
|
||||
paths = os.getenv('path').split(os.pathsep)
|
||||
for p in paths:
|
||||
fn = os.path.join(os.path.abspath(p), exe)
|
||||
if os.path.isfile(fn):
|
||||
return fn
|
||||
return exe
|
||||
|
||||
|
||||
_vcvars_names = {
|
||||
'win32': 'x86',
|
||||
'win-amd64': 'amd64',
|
||||
'win-arm32': 'arm',
|
||||
'win-arm64': 'arm64',
|
||||
}
|
||||
|
||||
|
||||
def _get_vcvars_spec(host_platform, platform):
|
||||
"""
|
||||
Given a host platform and platform, determine the spec for vcvarsall.
|
||||
|
||||
Uses the native MSVC host if the host platform would need expensive
|
||||
emulation for x86.
|
||||
|
||||
>>> _get_vcvars_spec('win-arm64', 'win32')
|
||||
'arm64_x86'
|
||||
>>> _get_vcvars_spec('win-arm64', 'win-amd64')
|
||||
'arm64_amd64'
|
||||
|
||||
Otherwise, always cross-compile from x86 to work with the
|
||||
lighter-weight MSVC installs that do not include native 64-bit tools.
|
||||
|
||||
>>> _get_vcvars_spec('win32', 'win32')
|
||||
'x86'
|
||||
>>> _get_vcvars_spec('win-arm32', 'win-arm32')
|
||||
'x86_arm'
|
||||
>>> _get_vcvars_spec('win-amd64', 'win-arm64')
|
||||
'x86_arm64'
|
||||
"""
|
||||
if host_platform != 'win-arm64':
|
||||
host_platform = 'win32'
|
||||
vc_hp = _vcvars_names[host_platform]
|
||||
vc_plat = _vcvars_names[platform]
|
||||
return vc_hp if vc_hp == vc_plat else f'{vc_hp}_{vc_plat}'
|
||||
|
||||
|
||||
class Compiler(base.Compiler):
|
||||
"""Concrete class that implements an interface to Microsoft Visual C++,
|
||||
as defined by the CCompiler abstract class."""
|
||||
|
||||
compiler_type = 'msvc'
|
||||
|
||||
# Just set this so CCompiler's constructor doesn't barf. We currently
|
||||
# don't use the 'set_executables()' bureaucracy provided by CCompiler,
|
||||
# as it really isn't necessary for this sort of single-compiler class.
|
||||
# Would be nice to have a consistent interface with UnixCCompiler,
|
||||
# though, so it's worth thinking about.
|
||||
executables = {}
|
||||
|
||||
# Private class data (need to distinguish C from C++ source for compiler)
|
||||
_c_extensions = ['.c']
|
||||
_cpp_extensions = ['.cc', '.cpp', '.cxx']
|
||||
_rc_extensions = ['.rc']
|
||||
_mc_extensions = ['.mc']
|
||||
|
||||
# Needed for the filename generation methods provided by the
|
||||
# base class, CCompiler.
|
||||
src_extensions = _c_extensions + _cpp_extensions + _rc_extensions + _mc_extensions
|
||||
res_extension = '.res'
|
||||
obj_extension = '.obj'
|
||||
static_lib_extension = '.lib'
|
||||
shared_lib_extension = '.dll'
|
||||
static_lib_format = shared_lib_format = '%s%s'
|
||||
exe_extension = '.exe'
|
||||
|
||||
def __init__(self, verbose=False, dry_run=False, force=False) -> None:
|
||||
super().__init__(verbose, dry_run, force)
|
||||
# target platform (.plat_name is consistent with 'bdist')
|
||||
self.plat_name = None
|
||||
self.initialized = False
|
||||
|
||||
@classmethod
|
||||
def _configure(cls, vc_env):
|
||||
"""
|
||||
Set class-level include/lib dirs.
|
||||
"""
|
||||
cls.include_dirs = cls._parse_path(vc_env.get('include', ''))
|
||||
cls.library_dirs = cls._parse_path(vc_env.get('lib', ''))
|
||||
|
||||
@staticmethod
|
||||
def _parse_path(val):
|
||||
return [dir.rstrip(os.sep) for dir in val.split(os.pathsep) if dir]
|
||||
|
||||
def initialize(self, plat_name: str | None = None) -> None:
|
||||
# multi-init means we would need to check platform same each time...
|
||||
assert not self.initialized, "don't init multiple times"
|
||||
if plat_name is None:
|
||||
plat_name = get_platform()
|
||||
# sanity check for platforms to prevent obscure errors later.
|
||||
if plat_name not in _vcvars_names:
|
||||
raise DistutilsPlatformError(
|
||||
f"--plat-name must be one of {tuple(_vcvars_names)}"
|
||||
)
|
||||
|
||||
plat_spec = _get_vcvars_spec(get_host_platform(), plat_name)
|
||||
|
||||
vc_env = _get_vc_env(plat_spec)
|
||||
if not vc_env:
|
||||
raise DistutilsPlatformError(
|
||||
"Unable to find a compatible Visual Studio installation."
|
||||
)
|
||||
self._configure(vc_env)
|
||||
|
||||
self._paths = vc_env.get('path', '')
|
||||
paths = self._paths.split(os.pathsep)
|
||||
self.cc = _find_exe("cl.exe", paths)
|
||||
self.linker = _find_exe("link.exe", paths)
|
||||
self.lib = _find_exe("lib.exe", paths)
|
||||
self.rc = _find_exe("rc.exe", paths) # resource compiler
|
||||
self.mc = _find_exe("mc.exe", paths) # message compiler
|
||||
self.mt = _find_exe("mt.exe", paths) # message compiler
|
||||
|
||||
self.preprocess_options = None
|
||||
# bpo-38597: Always compile with dynamic linking
|
||||
# Future releases of Python 3.x will include all past
|
||||
# versions of vcruntime*.dll for compatibility.
|
||||
self.compile_options = ['/nologo', '/O2', '/W3', '/GL', '/DNDEBUG', '/MD']
|
||||
|
||||
self.compile_options_debug = [
|
||||
'/nologo',
|
||||
'/Od',
|
||||
'/MDd',
|
||||
'/Zi',
|
||||
'/W3',
|
||||
'/D_DEBUG',
|
||||
]
|
||||
|
||||
ldflags = ['/nologo', '/INCREMENTAL:NO', '/LTCG']
|
||||
|
||||
ldflags_debug = ['/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL']
|
||||
|
||||
self.ldflags_exe = [*ldflags, '/MANIFEST:EMBED,ID=1']
|
||||
self.ldflags_exe_debug = [*ldflags_debug, '/MANIFEST:EMBED,ID=1']
|
||||
self.ldflags_shared = [
|
||||
*ldflags,
|
||||
'/DLL',
|
||||
'/MANIFEST:EMBED,ID=2',
|
||||
'/MANIFESTUAC:NO',
|
||||
]
|
||||
self.ldflags_shared_debug = [
|
||||
*ldflags_debug,
|
||||
'/DLL',
|
||||
'/MANIFEST:EMBED,ID=2',
|
||||
'/MANIFESTUAC:NO',
|
||||
]
|
||||
self.ldflags_static = [*ldflags]
|
||||
self.ldflags_static_debug = [*ldflags_debug]
|
||||
|
||||
self._ldflags = {
|
||||
(base.Compiler.EXECUTABLE, None): self.ldflags_exe,
|
||||
(base.Compiler.EXECUTABLE, False): self.ldflags_exe,
|
||||
(base.Compiler.EXECUTABLE, True): self.ldflags_exe_debug,
|
||||
(base.Compiler.SHARED_OBJECT, None): self.ldflags_shared,
|
||||
(base.Compiler.SHARED_OBJECT, False): self.ldflags_shared,
|
||||
(base.Compiler.SHARED_OBJECT, True): self.ldflags_shared_debug,
|
||||
(base.Compiler.SHARED_LIBRARY, None): self.ldflags_static,
|
||||
(base.Compiler.SHARED_LIBRARY, False): self.ldflags_static,
|
||||
(base.Compiler.SHARED_LIBRARY, True): self.ldflags_static_debug,
|
||||
}
|
||||
|
||||
self.initialized = True
|
||||
|
||||
# -- Worker methods ------------------------------------------------
|
||||
|
||||
@property
|
||||
def out_extensions(self) -> dict[str, str]:
|
||||
return {
|
||||
**super().out_extensions,
|
||||
**{
|
||||
ext: self.res_extension
|
||||
for ext in self._rc_extensions + self._mc_extensions
|
||||
},
|
||||
}
|
||||
|
||||
def compile( # noqa: C901
|
||||
self,
|
||||
sources,
|
||||
output_dir=None,
|
||||
macros=None,
|
||||
include_dirs=None,
|
||||
debug=False,
|
||||
extra_preargs=None,
|
||||
extra_postargs=None,
|
||||
depends=None,
|
||||
):
|
||||
if not self.initialized:
|
||||
self.initialize()
|
||||
compile_info = self._setup_compile(
|
||||
output_dir, macros, include_dirs, sources, depends, extra_postargs
|
||||
)
|
||||
macros, objects, extra_postargs, pp_opts, build = compile_info
|
||||
|
||||
compile_opts = extra_preargs or []
|
||||
compile_opts.append('/c')
|
||||
if debug:
|
||||
compile_opts.extend(self.compile_options_debug)
|
||||
else:
|
||||
compile_opts.extend(self.compile_options)
|
||||
|
||||
add_cpp_opts = False
|
||||
|
||||
for obj in objects:
|
||||
try:
|
||||
src, ext = build[obj]
|
||||
except KeyError:
|
||||
continue
|
||||
if debug:
|
||||
# pass the full pathname to MSVC in debug mode,
|
||||
# this allows the debugger to find the source file
|
||||
# without asking the user to browse for it
|
||||
src = os.path.abspath(src)
|
||||
|
||||
if ext in self._c_extensions:
|
||||
input_opt = f"/Tc{src}"
|
||||
elif ext in self._cpp_extensions:
|
||||
input_opt = f"/Tp{src}"
|
||||
add_cpp_opts = True
|
||||
elif ext in self._rc_extensions:
|
||||
# compile .RC to .RES file
|
||||
input_opt = src
|
||||
output_opt = "/fo" + obj
|
||||
try:
|
||||
self.spawn([self.rc] + pp_opts + [output_opt, input_opt])
|
||||
except DistutilsExecError as msg:
|
||||
raise CompileError(msg)
|
||||
continue
|
||||
elif ext in self._mc_extensions:
|
||||
# Compile .MC to .RC file to .RES file.
|
||||
# * '-h dir' specifies the directory for the
|
||||
# generated include file
|
||||
# * '-r dir' specifies the target directory of the
|
||||
# generated RC file and the binary message resource
|
||||
# it includes
|
||||
#
|
||||
# For now (since there are no options to change this),
|
||||
# we use the source-directory for the include file and
|
||||
# the build directory for the RC file and message
|
||||
# resources. This works at least for win32all.
|
||||
h_dir = os.path.dirname(src)
|
||||
rc_dir = os.path.dirname(obj)
|
||||
try:
|
||||
# first compile .MC to .RC and .H file
|
||||
self.spawn([self.mc, '-h', h_dir, '-r', rc_dir, src])
|
||||
base, _ = os.path.splitext(os.path.basename(src))
|
||||
rc_file = os.path.join(rc_dir, base + '.rc')
|
||||
# then compile .RC to .RES file
|
||||
self.spawn([self.rc, "/fo" + obj, rc_file])
|
||||
|
||||
except DistutilsExecError as msg:
|
||||
raise CompileError(msg)
|
||||
continue
|
||||
else:
|
||||
# how to handle this file?
|
||||
raise CompileError(f"Don't know how to compile {src} to {obj}")
|
||||
|
||||
args = [self.cc] + compile_opts + pp_opts
|
||||
if add_cpp_opts:
|
||||
args.append('/EHsc')
|
||||
args.extend((input_opt, "/Fo" + obj))
|
||||
args.extend(extra_postargs)
|
||||
|
||||
try:
|
||||
self.spawn(args)
|
||||
except DistutilsExecError as msg:
|
||||
raise CompileError(msg)
|
||||
|
||||
return objects
|
||||
|
||||
def create_static_lib(
|
||||
self,
|
||||
objects: list[str] | tuple[str, ...],
|
||||
output_libname: str,
|
||||
output_dir: str | None = None,
|
||||
debug: bool = False,
|
||||
target_lang: str | None = None,
|
||||
) -> None:
|
||||
if not self.initialized:
|
||||
self.initialize()
|
||||
objects, output_dir = self._fix_object_args(objects, output_dir)
|
||||
output_filename = self.library_filename(output_libname, output_dir=output_dir)
|
||||
|
||||
if self._need_link(objects, output_filename):
|
||||
lib_args = objects + ['/OUT:' + output_filename]
|
||||
if debug:
|
||||
pass # XXX what goes here?
|
||||
try:
|
||||
log.debug('Executing "%s" %s', self.lib, ' '.join(lib_args))
|
||||
self.spawn([self.lib] + lib_args)
|
||||
except DistutilsExecError as msg:
|
||||
raise LibError(msg)
|
||||
else:
|
||||
log.debug("skipping %s (up-to-date)", output_filename)
|
||||
|
||||
def link(
|
||||
self,
|
||||
target_desc: str,
|
||||
objects: list[str] | tuple[str, ...],
|
||||
output_filename: str,
|
||||
output_dir: str | None = None,
|
||||
libraries: list[str] | tuple[str, ...] | None = None,
|
||||
library_dirs: list[str] | tuple[str, ...] | None = None,
|
||||
runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
|
||||
export_symbols: Iterable[str] | None = None,
|
||||
debug: bool = False,
|
||||
extra_preargs: list[str] | None = None,
|
||||
extra_postargs: Iterable[str] | None = None,
|
||||
build_temp: str | os.PathLike[str] | None = None,
|
||||
target_lang: str | None = None,
|
||||
) -> None:
|
||||
if not self.initialized:
|
||||
self.initialize()
|
||||
objects, output_dir = self._fix_object_args(objects, output_dir)
|
||||
fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)
|
||||
libraries, library_dirs, runtime_library_dirs = fixed_args
|
||||
|
||||
if runtime_library_dirs:
|
||||
self.warn(
|
||||
"I don't know what to do with 'runtime_library_dirs': "
|
||||
+ str(runtime_library_dirs)
|
||||
)
|
||||
|
||||
lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)
|
||||
if output_dir is not None:
|
||||
output_filename = os.path.join(output_dir, output_filename)
|
||||
|
||||
if self._need_link(objects, output_filename):
|
||||
ldflags = self._ldflags[target_desc, debug]
|
||||
|
||||
export_opts = ["/EXPORT:" + sym for sym in (export_symbols or [])]
|
||||
|
||||
ld_args = (
|
||||
ldflags + lib_opts + export_opts + objects + ['/OUT:' + output_filename]
|
||||
)
|
||||
|
||||
# The MSVC linker generates .lib and .exp files, which cannot be
|
||||
# suppressed by any linker switches. The .lib files may even be
|
||||
# needed! Make sure they are generated in the temporary build
|
||||
# directory. Since they have different names for debug and release
|
||||
# builds, they can go into the same directory.
|
||||
build_temp = os.path.dirname(objects[0])
|
||||
if export_symbols is not None:
|
||||
(dll_name, dll_ext) = os.path.splitext(
|
||||
os.path.basename(output_filename)
|
||||
)
|
||||
implib_file = os.path.join(build_temp, self.library_filename(dll_name))
|
||||
ld_args.append('/IMPLIB:' + implib_file)
|
||||
|
||||
if extra_preargs:
|
||||
ld_args[:0] = extra_preargs
|
||||
if extra_postargs:
|
||||
ld_args.extend(extra_postargs)
|
||||
|
||||
output_dir = os.path.dirname(os.path.abspath(output_filename))
|
||||
self.mkpath(output_dir)
|
||||
try:
|
||||
log.debug('Executing "%s" %s', self.linker, ' '.join(ld_args))
|
||||
self.spawn([self.linker] + ld_args)
|
||||
except DistutilsExecError as msg:
|
||||
raise LinkError(msg)
|
||||
else:
|
||||
log.debug("skipping %s (up-to-date)", output_filename)
|
||||
|
||||
def spawn(self, cmd):
|
||||
env = dict(os.environ, PATH=self._paths)
|
||||
with self._fallback_spawn(cmd, env) as fallback:
|
||||
return super().spawn(cmd, env=env)
|
||||
return fallback.value
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _fallback_spawn(self, cmd, env):
|
||||
"""
|
||||
Discovered in pypa/distutils#15, some tools monkeypatch the compiler,
|
||||
so the 'env' kwarg causes a TypeError. Detect this condition and
|
||||
restore the legacy, unsafe behavior.
|
||||
"""
|
||||
bag = type('Bag', (), {})()
|
||||
try:
|
||||
yield bag
|
||||
except TypeError as exc:
|
||||
if "unexpected keyword argument 'env'" not in str(exc):
|
||||
raise
|
||||
else:
|
||||
return
|
||||
warnings.warn("Fallback spawn triggered. Please update distutils monkeypatch.")
|
||||
with mock.patch.dict('os.environ', env):
|
||||
bag.value = super().spawn(cmd)
|
||||
|
||||
# -- Miscellaneous methods -----------------------------------------
|
||||
# These are all used by the 'gen_lib_options() function, in
|
||||
# ccompiler.py.
|
||||
|
||||
def library_dir_option(self, dir):
|
||||
return "/LIBPATH:" + dir
|
||||
|
||||
def runtime_library_dir_option(self, dir):
|
||||
raise DistutilsPlatformError(
|
||||
"don't know how to set runtime library search path for MSVC"
|
||||
)
|
||||
|
||||
def library_option(self, lib):
|
||||
return self.library_filename(lib)
|
||||
|
||||
def find_library_file(self, dirs, lib, debug=False):
|
||||
# Prefer a debugging library if found (and requested), but deal
|
||||
# with it if we don't have one.
|
||||
if debug:
|
||||
try_names = [lib + "_d", lib]
|
||||
else:
|
||||
try_names = [lib]
|
||||
for dir in dirs:
|
||||
for name in try_names:
|
||||
libfile = os.path.join(dir, self.library_filename(name))
|
||||
if os.path.isfile(libfile):
|
||||
return libfile
|
||||
else:
|
||||
# Oops, didn't find it in *any* of 'dirs'
|
||||
return None
|
||||
@@ -0,0 +1,422 @@
|
||||
"""distutils.unixccompiler
|
||||
|
||||
Contains the UnixCCompiler class, a subclass of CCompiler that handles
|
||||
the "typical" Unix-style command-line C compiler:
|
||||
* macros defined with -Dname[=value]
|
||||
* macros undefined with -Uname
|
||||
* include search directories specified with -Idir
|
||||
* libraries specified with -lllib
|
||||
* library search directories specified with -Ldir
|
||||
* compile handled by 'cc' (or similar) executable with -c option:
|
||||
compiles .c to .o
|
||||
* link static library handled by 'ar' command (possibly with 'ranlib')
|
||||
* link shared library handled by 'cc -shared'
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
|
||||
from ... import sysconfig
|
||||
from ..._log import log
|
||||
from ..._macos_compat import compiler_fixup
|
||||
from ..._modified import newer
|
||||
from ...compat import consolidate_linker_args
|
||||
from ...errors import DistutilsExecError
|
||||
from . import base
|
||||
from .base import _Macro, gen_lib_options, gen_preprocess_options
|
||||
from .errors import (
|
||||
CompileError,
|
||||
LibError,
|
||||
LinkError,
|
||||
)
|
||||
|
||||
# XXX Things not currently handled:
|
||||
# * optimization/debug/warning flags; we just use whatever's in Python's
|
||||
# Makefile and live with it. Is this adequate? If not, we might
|
||||
# have to have a bunch of subclasses GNUCCompiler, SGICCompiler,
|
||||
# SunCCompiler, and I suspect down that road lies madness.
|
||||
# * even if we don't know a warning flag from an optimization flag,
|
||||
# we need some way for outsiders to feed preprocessor/compiler/linker
|
||||
# flags in to us -- eg. a sysadmin might want to mandate certain flags
|
||||
# via a site config file, or a user might want to set something for
|
||||
# compiling this module distribution only via the setup.py command
|
||||
# line, whatever. As long as these options come from something on the
|
||||
# current system, they can be as system-dependent as they like, and we
|
||||
# should just happily stuff them into the preprocessor/compiler/linker
|
||||
# options and carry on.
|
||||
|
||||
|
||||
def _split_env(cmd):
|
||||
"""
|
||||
For macOS, split command into 'env' portion (if any)
|
||||
and the rest of the linker command.
|
||||
|
||||
>>> _split_env(['a', 'b', 'c'])
|
||||
([], ['a', 'b', 'c'])
|
||||
>>> _split_env(['/usr/bin/env', 'A=3', 'gcc'])
|
||||
(['/usr/bin/env', 'A=3'], ['gcc'])
|
||||
"""
|
||||
pivot = 0
|
||||
if os.path.basename(cmd[0]) == "env":
|
||||
pivot = 1
|
||||
while '=' in cmd[pivot]:
|
||||
pivot += 1
|
||||
return cmd[:pivot], cmd[pivot:]
|
||||
|
||||
|
||||
def _split_aix(cmd):
|
||||
"""
|
||||
AIX platforms prefix the compiler with the ld_so_aix
|
||||
script, so split that from the linker command.
|
||||
|
||||
>>> _split_aix(['a', 'b', 'c'])
|
||||
([], ['a', 'b', 'c'])
|
||||
>>> _split_aix(['/bin/foo/ld_so_aix', 'gcc'])
|
||||
(['/bin/foo/ld_so_aix'], ['gcc'])
|
||||
"""
|
||||
pivot = os.path.basename(cmd[0]) == 'ld_so_aix'
|
||||
return cmd[:pivot], cmd[pivot:]
|
||||
|
||||
|
||||
def _linker_params(linker_cmd, compiler_cmd):
|
||||
"""
|
||||
The linker command usually begins with the compiler
|
||||
command (possibly multiple elements), followed by zero or more
|
||||
params for shared library building.
|
||||
|
||||
If the LDSHARED env variable overrides the linker command,
|
||||
however, the commands may not match.
|
||||
|
||||
Return the best guess of the linker parameters by stripping
|
||||
the linker command. If the compiler command does not
|
||||
match the linker command, assume the linker command is
|
||||
just the first element.
|
||||
|
||||
>>> _linker_params('gcc foo bar'.split(), ['gcc'])
|
||||
['foo', 'bar']
|
||||
>>> _linker_params('gcc foo bar'.split(), ['other'])
|
||||
['foo', 'bar']
|
||||
>>> _linker_params('ccache gcc foo bar'.split(), 'ccache gcc'.split())
|
||||
['foo', 'bar']
|
||||
>>> _linker_params(['gcc'], ['gcc'])
|
||||
[]
|
||||
"""
|
||||
c_len = len(compiler_cmd)
|
||||
pivot = c_len if linker_cmd[:c_len] == compiler_cmd else 1
|
||||
return linker_cmd[pivot:]
|
||||
|
||||
|
||||
class Compiler(base.Compiler):
|
||||
compiler_type = 'unix'
|
||||
|
||||
# These are used by CCompiler in two places: the constructor sets
|
||||
# instance attributes 'preprocessor', 'compiler', etc. from them, and
|
||||
# 'set_executable()' allows any of these to be set. The defaults here
|
||||
# are pretty generic; they will probably have to be set by an outsider
|
||||
# (eg. using information discovered by the sysconfig about building
|
||||
# Python extensions).
|
||||
executables = {
|
||||
'preprocessor': None,
|
||||
'compiler': ["cc"],
|
||||
'compiler_so': ["cc"],
|
||||
'compiler_cxx': ["c++"],
|
||||
'compiler_so_cxx': ["c++"],
|
||||
'linker_so': ["cc", "-shared"],
|
||||
'linker_so_cxx': ["c++", "-shared"],
|
||||
'linker_exe': ["cc"],
|
||||
'linker_exe_cxx': ["c++", "-shared"],
|
||||
'archiver': ["ar", "-cr"],
|
||||
'ranlib': None,
|
||||
}
|
||||
|
||||
if sys.platform[:6] == "darwin":
|
||||
executables['ranlib'] = ["ranlib"]
|
||||
|
||||
# Needed for the filename generation methods provided by the base
|
||||
# class, CCompiler. NB. whoever instantiates/uses a particular
|
||||
# UnixCCompiler instance should set 'shared_lib_ext' -- we set a
|
||||
# reasonable common default here, but it's not necessarily used on all
|
||||
# Unices!
|
||||
|
||||
src_extensions = [".c", ".C", ".cc", ".cxx", ".cpp", ".m"]
|
||||
obj_extension = ".o"
|
||||
static_lib_extension = ".a"
|
||||
shared_lib_extension = ".so"
|
||||
dylib_lib_extension = ".dylib"
|
||||
xcode_stub_lib_extension = ".tbd"
|
||||
static_lib_format = shared_lib_format = dylib_lib_format = "lib%s%s"
|
||||
xcode_stub_lib_format = dylib_lib_format
|
||||
if sys.platform == "cygwin":
|
||||
exe_extension = ".exe"
|
||||
shared_lib_extension = ".dll.a"
|
||||
dylib_lib_extension = ".dll"
|
||||
dylib_lib_format = "cyg%s%s"
|
||||
|
||||
def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs):
|
||||
"""Remove standard library path from rpath"""
|
||||
libraries, library_dirs, runtime_library_dirs = super()._fix_lib_args(
|
||||
libraries, library_dirs, runtime_library_dirs
|
||||
)
|
||||
libdir = sysconfig.get_config_var('LIBDIR')
|
||||
if (
|
||||
runtime_library_dirs
|
||||
and libdir.startswith("/usr/lib")
|
||||
and (libdir in runtime_library_dirs)
|
||||
):
|
||||
runtime_library_dirs.remove(libdir)
|
||||
return libraries, library_dirs, runtime_library_dirs
|
||||
|
||||
def preprocess(
|
||||
self,
|
||||
source: str | os.PathLike[str],
|
||||
output_file: str | os.PathLike[str] | None = None,
|
||||
macros: list[_Macro] | None = None,
|
||||
include_dirs: list[str] | tuple[str, ...] | None = None,
|
||||
extra_preargs: list[str] | None = None,
|
||||
extra_postargs: Iterable[str] | None = None,
|
||||
):
|
||||
fixed_args = self._fix_compile_args(None, macros, include_dirs)
|
||||
ignore, macros, include_dirs = fixed_args
|
||||
pp_opts = gen_preprocess_options(macros, include_dirs)
|
||||
pp_args = self.preprocessor + pp_opts
|
||||
if output_file:
|
||||
pp_args.extend(['-o', output_file])
|
||||
if extra_preargs:
|
||||
pp_args[:0] = extra_preargs
|
||||
if extra_postargs:
|
||||
pp_args.extend(extra_postargs)
|
||||
pp_args.append(source)
|
||||
|
||||
# reasons to preprocess:
|
||||
# - force is indicated
|
||||
# - output is directed to stdout
|
||||
# - source file is newer than the target
|
||||
preprocess = self.force or output_file is None or newer(source, output_file)
|
||||
if not preprocess:
|
||||
return
|
||||
|
||||
if output_file:
|
||||
self.mkpath(os.path.dirname(output_file))
|
||||
|
||||
try:
|
||||
self.spawn(pp_args)
|
||||
except DistutilsExecError as msg:
|
||||
raise CompileError(msg)
|
||||
|
||||
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
|
||||
compiler_so = compiler_fixup(self.compiler_so, cc_args + extra_postargs)
|
||||
compiler_so_cxx = compiler_fixup(self.compiler_so_cxx, cc_args + extra_postargs)
|
||||
try:
|
||||
if self.detect_language(src) == 'c++':
|
||||
self.spawn(
|
||||
compiler_so_cxx + cc_args + [src, '-o', obj] + extra_postargs
|
||||
)
|
||||
else:
|
||||
self.spawn(compiler_so + cc_args + [src, '-o', obj] + extra_postargs)
|
||||
except DistutilsExecError as msg:
|
||||
raise CompileError(msg)
|
||||
|
||||
def create_static_lib(
|
||||
self, objects, output_libname, output_dir=None, debug=False, target_lang=None
|
||||
):
|
||||
objects, output_dir = self._fix_object_args(objects, output_dir)
|
||||
|
||||
output_filename = self.library_filename(output_libname, output_dir=output_dir)
|
||||
|
||||
if self._need_link(objects, output_filename):
|
||||
self.mkpath(os.path.dirname(output_filename))
|
||||
self.spawn(self.archiver + [output_filename] + objects + self.objects)
|
||||
|
||||
# Not many Unices required ranlib anymore -- SunOS 4.x is, I
|
||||
# think the only major Unix that does. Maybe we need some
|
||||
# platform intelligence here to skip ranlib if it's not
|
||||
# needed -- or maybe Python's configure script took care of
|
||||
# it for us, hence the check for leading colon.
|
||||
if self.ranlib:
|
||||
try:
|
||||
self.spawn(self.ranlib + [output_filename])
|
||||
except DistutilsExecError as msg:
|
||||
raise LibError(msg)
|
||||
else:
|
||||
log.debug("skipping %s (up-to-date)", output_filename)
|
||||
|
||||
def link(
|
||||
self,
|
||||
target_desc,
|
||||
objects: list[str] | tuple[str, ...],
|
||||
output_filename,
|
||||
output_dir: str | None = None,
|
||||
libraries: list[str] | tuple[str, ...] | None = None,
|
||||
library_dirs: list[str] | tuple[str, ...] | None = None,
|
||||
runtime_library_dirs: list[str] | tuple[str, ...] | None = None,
|
||||
export_symbols=None,
|
||||
debug=False,
|
||||
extra_preargs=None,
|
||||
extra_postargs=None,
|
||||
build_temp=None,
|
||||
target_lang=None,
|
||||
):
|
||||
objects, output_dir = self._fix_object_args(objects, output_dir)
|
||||
fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)
|
||||
libraries, library_dirs, runtime_library_dirs = fixed_args
|
||||
|
||||
lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)
|
||||
if not isinstance(output_dir, (str, type(None))):
|
||||
raise TypeError("'output_dir' must be a string or None")
|
||||
if output_dir is not None:
|
||||
output_filename = os.path.join(output_dir, output_filename)
|
||||
|
||||
if self._need_link(objects, output_filename):
|
||||
ld_args = objects + self.objects + lib_opts + ['-o', output_filename]
|
||||
if debug:
|
||||
ld_args[:0] = ['-g']
|
||||
if extra_preargs:
|
||||
ld_args[:0] = extra_preargs
|
||||
if extra_postargs:
|
||||
ld_args.extend(extra_postargs)
|
||||
self.mkpath(os.path.dirname(output_filename))
|
||||
try:
|
||||
# Select a linker based on context: linker_exe when
|
||||
# building an executable or linker_so (with shared options)
|
||||
# when building a shared library.
|
||||
building_exe = target_desc == base.Compiler.EXECUTABLE
|
||||
target_cxx = target_lang == "c++"
|
||||
linker = (
|
||||
(self.linker_exe_cxx if target_cxx else self.linker_exe)
|
||||
if building_exe
|
||||
else (self.linker_so_cxx if target_cxx else self.linker_so)
|
||||
)[:]
|
||||
|
||||
if target_cxx and self.compiler_cxx:
|
||||
env, linker_ne = _split_env(linker)
|
||||
aix, linker_na = _split_aix(linker_ne)
|
||||
_, compiler_cxx_ne = _split_env(self.compiler_cxx)
|
||||
_, linker_exe_ne = _split_env(self.linker_exe_cxx)
|
||||
|
||||
params = _linker_params(linker_na, linker_exe_ne)
|
||||
linker = env + aix + compiler_cxx_ne + params
|
||||
|
||||
linker = compiler_fixup(linker, ld_args)
|
||||
|
||||
self.spawn(linker + ld_args)
|
||||
except DistutilsExecError as msg:
|
||||
raise LinkError(msg)
|
||||
else:
|
||||
log.debug("skipping %s (up-to-date)", output_filename)
|
||||
|
||||
# -- Miscellaneous methods -----------------------------------------
|
||||
# These are all used by the 'gen_lib_options() function, in
|
||||
# ccompiler.py.
|
||||
|
||||
def library_dir_option(self, dir):
|
||||
return "-L" + dir
|
||||
|
||||
def _is_gcc(self):
|
||||
cc_var = sysconfig.get_config_var("CC")
|
||||
compiler = os.path.basename(shlex.split(cc_var)[0])
|
||||
return "gcc" in compiler or "g++" in compiler
|
||||
|
||||
def runtime_library_dir_option(self, dir: str) -> str | list[str]: # type: ignore[override] # Fixed in pypa/distutils#339
|
||||
# XXX Hackish, at the very least. See Python bug #445902:
|
||||
# https://bugs.python.org/issue445902
|
||||
# Linkers on different platforms need different options to
|
||||
# specify that directories need to be added to the list of
|
||||
# directories searched for dependencies when a dynamic library
|
||||
# is sought. GCC on GNU systems (Linux, FreeBSD, ...) has to
|
||||
# be told to pass the -R option through to the linker, whereas
|
||||
# other compilers and gcc on other systems just know this.
|
||||
# Other compilers may need something slightly different. At
|
||||
# this time, there's no way to determine this information from
|
||||
# the configuration data stored in the Python installation, so
|
||||
# we use this hack.
|
||||
if sys.platform[:6] == "darwin":
|
||||
from distutils.util import get_macosx_target_ver, split_version
|
||||
|
||||
macosx_target_ver = get_macosx_target_ver()
|
||||
if macosx_target_ver and split_version(macosx_target_ver) >= [10, 5]:
|
||||
return "-Wl,-rpath," + dir
|
||||
else: # no support for -rpath on earlier macOS versions
|
||||
return "-L" + dir
|
||||
elif sys.platform[:7] == "freebsd":
|
||||
return "-Wl,-rpath=" + dir
|
||||
elif sys.platform[:5] == "hp-ux":
|
||||
return [
|
||||
"-Wl,+s" if self._is_gcc() else "+s",
|
||||
"-L" + dir,
|
||||
]
|
||||
|
||||
# For all compilers, `-Wl` is the presumed way to pass a
|
||||
# compiler option to the linker
|
||||
if sysconfig.get_config_var("GNULD") == "yes":
|
||||
return consolidate_linker_args([
|
||||
# Force RUNPATH instead of RPATH
|
||||
"-Wl,--enable-new-dtags",
|
||||
"-Wl,-rpath," + dir,
|
||||
])
|
||||
else:
|
||||
return "-Wl,-R" + dir
|
||||
|
||||
def library_option(self, lib):
|
||||
return "-l" + lib
|
||||
|
||||
@staticmethod
|
||||
def _library_root(dir):
|
||||
"""
|
||||
macOS users can specify an alternate SDK using'-isysroot'.
|
||||
Calculate the SDK root if it is specified.
|
||||
|
||||
Note that, as of Xcode 7, Apple SDKs may contain textual stub
|
||||
libraries with .tbd extensions rather than the normal .dylib
|
||||
shared libraries installed in /. The Apple compiler tool
|
||||
chain handles this transparently but it can cause problems
|
||||
for programs that are being built with an SDK and searching
|
||||
for specific libraries. Callers of find_library_file need to
|
||||
keep in mind that the base filename of the returned SDK library
|
||||
file might have a different extension from that of the library
|
||||
file installed on the running system, for example:
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/
|
||||
MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/
|
||||
usr/lib/libedit.tbd
|
||||
vs
|
||||
/usr/lib/libedit.dylib
|
||||
"""
|
||||
cflags = sysconfig.get_config_var('CFLAGS')
|
||||
match = re.search(r'-isysroot\s*(\S+)', cflags)
|
||||
|
||||
apply_root = (
|
||||
sys.platform == 'darwin'
|
||||
and match
|
||||
and (
|
||||
dir.startswith('/System/')
|
||||
or (dir.startswith('/usr/') and not dir.startswith('/usr/local/'))
|
||||
)
|
||||
)
|
||||
|
||||
return os.path.join(match.group(1), dir[1:]) if apply_root else dir
|
||||
|
||||
def find_library_file(self, dirs, lib, debug=False):
|
||||
"""
|
||||
Second-guess the linker with not much hard
|
||||
data to go on: GCC seems to prefer the shared library, so
|
||||
assume that *all* Unix C compilers do,
|
||||
ignoring even GCC's "-static" option.
|
||||
"""
|
||||
lib_names = (
|
||||
self.library_filename(lib, lib_type=type)
|
||||
for type in 'dylib xcode_stub shared static'.split()
|
||||
)
|
||||
|
||||
roots = map(self._library_root, dirs)
|
||||
|
||||
searched = itertools.starmap(os.path.join, itertools.product(roots, lib_names))
|
||||
|
||||
found = filter(os.path.exists, searched)
|
||||
|
||||
# Return None if it could not be found in any dir.
|
||||
return next(found, None)
|
||||
230
.venv/Lib/site-packages/setuptools/_distutils/compilers/C/zos.py
Normal file
230
.venv/Lib/site-packages/setuptools/_distutils/compilers/C/zos.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""distutils.zosccompiler
|
||||
|
||||
Contains the selection of the c & c++ compilers on z/OS. There are several
|
||||
different c compilers on z/OS, all of them are optional, so the correct
|
||||
one needs to be chosen based on the users input. This is compatible with
|
||||
the following compilers:
|
||||
|
||||
IBM C/C++ For Open Enterprise Languages on z/OS 2.0
|
||||
IBM Open XL C/C++ 1.1 for z/OS
|
||||
IBM XL C/C++ V2.4.1 for z/OS 2.4 and 2.5
|
||||
IBM z/OS XL C/C++
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ... import sysconfig
|
||||
from ...errors import DistutilsExecError
|
||||
from . import unix
|
||||
from .errors import CompileError
|
||||
|
||||
_cc_args = {
|
||||
'ibm-openxl': [
|
||||
'-m64',
|
||||
'-fvisibility=default',
|
||||
'-fzos-le-char-mode=ascii',
|
||||
'-fno-short-enums',
|
||||
],
|
||||
'ibm-xlclang': [
|
||||
'-q64',
|
||||
'-qexportall',
|
||||
'-qascii',
|
||||
'-qstrict',
|
||||
'-qnocsect',
|
||||
'-Wa,asa,goff',
|
||||
'-Wa,xplink',
|
||||
'-qgonumber',
|
||||
'-qenum=int',
|
||||
'-Wc,DLL',
|
||||
],
|
||||
'ibm-xlc': [
|
||||
'-q64',
|
||||
'-qexportall',
|
||||
'-qascii',
|
||||
'-qstrict',
|
||||
'-qnocsect',
|
||||
'-Wa,asa,goff',
|
||||
'-Wa,xplink',
|
||||
'-qgonumber',
|
||||
'-qenum=int',
|
||||
'-Wc,DLL',
|
||||
'-qlanglvl=extc99',
|
||||
],
|
||||
}
|
||||
|
||||
_cxx_args = {
|
||||
'ibm-openxl': [
|
||||
'-m64',
|
||||
'-fvisibility=default',
|
||||
'-fzos-le-char-mode=ascii',
|
||||
'-fno-short-enums',
|
||||
],
|
||||
'ibm-xlclang': [
|
||||
'-q64',
|
||||
'-qexportall',
|
||||
'-qascii',
|
||||
'-qstrict',
|
||||
'-qnocsect',
|
||||
'-Wa,asa,goff',
|
||||
'-Wa,xplink',
|
||||
'-qgonumber',
|
||||
'-qenum=int',
|
||||
'-Wc,DLL',
|
||||
],
|
||||
'ibm-xlc': [
|
||||
'-q64',
|
||||
'-qexportall',
|
||||
'-qascii',
|
||||
'-qstrict',
|
||||
'-qnocsect',
|
||||
'-Wa,asa,goff',
|
||||
'-Wa,xplink',
|
||||
'-qgonumber',
|
||||
'-qenum=int',
|
||||
'-Wc,DLL',
|
||||
'-qlanglvl=extended0x',
|
||||
],
|
||||
}
|
||||
|
||||
_asm_args = {
|
||||
'ibm-openxl': ['-fasm', '-fno-integrated-as', '-Wa,--ASA', '-Wa,--GOFF'],
|
||||
'ibm-xlclang': [],
|
||||
'ibm-xlc': [],
|
||||
}
|
||||
|
||||
_ld_args = {
|
||||
'ibm-openxl': [],
|
||||
'ibm-xlclang': ['-Wl,dll', '-q64'],
|
||||
'ibm-xlc': ['-Wl,dll', '-q64'],
|
||||
}
|
||||
|
||||
|
||||
# Python on z/OS is built with no compiler specific options in it's CFLAGS.
|
||||
# But each compiler requires it's own specific options to build successfully,
|
||||
# though some of the options are common between them
|
||||
class Compiler(unix.Compiler):
|
||||
src_extensions = ['.c', '.C', '.cc', '.cxx', '.cpp', '.m', '.s']
|
||||
_cpp_extensions = ['.cc', '.cpp', '.cxx', '.C']
|
||||
_asm_extensions = ['.s']
|
||||
|
||||
def _get_zos_compiler_name(self):
|
||||
zos_compiler_names = [
|
||||
os.path.basename(binary)
|
||||
for envvar in ('CC', 'CXX', 'LDSHARED')
|
||||
if (binary := os.environ.get(envvar, None))
|
||||
]
|
||||
if len(zos_compiler_names) == 0:
|
||||
return 'ibm-openxl'
|
||||
|
||||
zos_compilers = {}
|
||||
for compiler in (
|
||||
'ibm-clang',
|
||||
'ibm-clang64',
|
||||
'ibm-clang++',
|
||||
'ibm-clang++64',
|
||||
'clang',
|
||||
'clang++',
|
||||
'clang-14',
|
||||
):
|
||||
zos_compilers[compiler] = 'ibm-openxl'
|
||||
|
||||
for compiler in ('xlclang', 'xlclang++', 'njsc', 'njsc++'):
|
||||
zos_compilers[compiler] = 'ibm-xlclang'
|
||||
|
||||
for compiler in ('xlc', 'xlC', 'xlc++'):
|
||||
zos_compilers[compiler] = 'ibm-xlc'
|
||||
|
||||
return zos_compilers.get(zos_compiler_names[0], 'ibm-openxl')
|
||||
|
||||
def __init__(self, verbose=False, dry_run=False, force=False):
|
||||
super().__init__(verbose, dry_run, force)
|
||||
self.zos_compiler = self._get_zos_compiler_name()
|
||||
sysconfig.customize_compiler(self)
|
||||
|
||||
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
|
||||
local_args = []
|
||||
if ext in self._cpp_extensions:
|
||||
compiler = self.compiler_cxx
|
||||
local_args.extend(_cxx_args[self.zos_compiler])
|
||||
elif ext in self._asm_extensions:
|
||||
compiler = self.compiler_so
|
||||
local_args.extend(_cc_args[self.zos_compiler])
|
||||
local_args.extend(_asm_args[self.zos_compiler])
|
||||
else:
|
||||
compiler = self.compiler_so
|
||||
local_args.extend(_cc_args[self.zos_compiler])
|
||||
local_args.extend(cc_args)
|
||||
|
||||
try:
|
||||
self.spawn(compiler + local_args + [src, '-o', obj] + extra_postargs)
|
||||
except DistutilsExecError as msg:
|
||||
raise CompileError(msg)
|
||||
|
||||
def runtime_library_dir_option(self, dir):
|
||||
return '-L' + dir
|
||||
|
||||
def link(
|
||||
self,
|
||||
target_desc,
|
||||
objects,
|
||||
output_filename,
|
||||
output_dir=None,
|
||||
libraries=None,
|
||||
library_dirs=None,
|
||||
runtime_library_dirs=None,
|
||||
export_symbols=None,
|
||||
debug=False,
|
||||
extra_preargs=None,
|
||||
extra_postargs=None,
|
||||
build_temp=None,
|
||||
target_lang=None,
|
||||
):
|
||||
# For a built module to use functions from cpython, it needs to use Pythons
|
||||
# side deck file. The side deck is located beside the libpython3.xx.so
|
||||
ldversion = sysconfig.get_config_var('LDVERSION')
|
||||
if sysconfig.python_build:
|
||||
side_deck_path = os.path.join(
|
||||
sysconfig.get_config_var('abs_builddir'),
|
||||
f'libpython{ldversion}.x',
|
||||
)
|
||||
else:
|
||||
side_deck_path = os.path.join(
|
||||
sysconfig.get_config_var('installed_base'),
|
||||
sysconfig.get_config_var('platlibdir'),
|
||||
f'libpython{ldversion}.x',
|
||||
)
|
||||
|
||||
if os.path.exists(side_deck_path):
|
||||
if extra_postargs:
|
||||
extra_postargs.append(side_deck_path)
|
||||
else:
|
||||
extra_postargs = [side_deck_path]
|
||||
|
||||
# Check and replace libraries included side deck files
|
||||
if runtime_library_dirs:
|
||||
for dir in runtime_library_dirs:
|
||||
for library in libraries[:]:
|
||||
library_side_deck = os.path.join(dir, f'{library}.x')
|
||||
if os.path.exists(library_side_deck):
|
||||
libraries.remove(library)
|
||||
extra_postargs.append(library_side_deck)
|
||||
break
|
||||
|
||||
# Any required ld args for the given compiler
|
||||
extra_postargs.extend(_ld_args[self.zos_compiler])
|
||||
|
||||
super().link(
|
||||
target_desc,
|
||||
objects,
|
||||
output_filename,
|
||||
output_dir,
|
||||
libraries,
|
||||
library_dirs,
|
||||
runtime_library_dirs,
|
||||
export_symbols,
|
||||
debug,
|
||||
extra_preargs,
|
||||
extra_postargs,
|
||||
build_temp,
|
||||
target_lang,
|
||||
)
|
||||
289
.venv/Lib/site-packages/setuptools/_distutils/core.py
Normal file
289
.venv/Lib/site-packages/setuptools/_distutils/core.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""distutils.core
|
||||
|
||||
The only module that needs to be imported to use the Distutils; provides
|
||||
the 'setup' function (which is to be called from the setup script). Also
|
||||
indirectly provides the Distribution and Command classes, although they are
|
||||
really defined in distutils.dist and distutils.cmd.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tokenize
|
||||
from collections.abc import Iterable
|
||||
|
||||
from .cmd import Command
|
||||
from .debug import DEBUG
|
||||
|
||||
# Mainly import these so setup scripts can "from distutils.core import" them.
|
||||
from .dist import Distribution
|
||||
from .errors import (
|
||||
CCompilerError,
|
||||
DistutilsArgError,
|
||||
DistutilsError,
|
||||
DistutilsSetupError,
|
||||
)
|
||||
from .extension import Extension
|
||||
|
||||
__all__ = ['Distribution', 'Command', 'Extension', 'setup']
|
||||
|
||||
# This is a barebones help message generated displayed when the user
|
||||
# runs the setup script with no arguments at all. More useful help
|
||||
# is generated with various --help options: global help, list commands,
|
||||
# and per-command help.
|
||||
USAGE = """\
|
||||
usage: %(script)s [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
|
||||
or: %(script)s --help [cmd1 cmd2 ...]
|
||||
or: %(script)s --help-commands
|
||||
or: %(script)s cmd --help
|
||||
"""
|
||||
|
||||
|
||||
def gen_usage(script_name):
|
||||
script = os.path.basename(script_name)
|
||||
return USAGE % locals()
|
||||
|
||||
|
||||
# Some mild magic to control the behaviour of 'setup()' from 'run_setup()'.
|
||||
_setup_stop_after = None
|
||||
_setup_distribution = None
|
||||
|
||||
# Legal keyword arguments for the setup() function
|
||||
setup_keywords = (
|
||||
'distclass',
|
||||
'script_name',
|
||||
'script_args',
|
||||
'options',
|
||||
'name',
|
||||
'version',
|
||||
'author',
|
||||
'author_email',
|
||||
'maintainer',
|
||||
'maintainer_email',
|
||||
'url',
|
||||
'license',
|
||||
'description',
|
||||
'long_description',
|
||||
'keywords',
|
||||
'platforms',
|
||||
'classifiers',
|
||||
'download_url',
|
||||
'requires',
|
||||
'provides',
|
||||
'obsoletes',
|
||||
)
|
||||
|
||||
# Legal keyword arguments for the Extension constructor
|
||||
extension_keywords = (
|
||||
'name',
|
||||
'sources',
|
||||
'include_dirs',
|
||||
'define_macros',
|
||||
'undef_macros',
|
||||
'library_dirs',
|
||||
'libraries',
|
||||
'runtime_library_dirs',
|
||||
'extra_objects',
|
||||
'extra_compile_args',
|
||||
'extra_link_args',
|
||||
'swig_opts',
|
||||
'export_symbols',
|
||||
'depends',
|
||||
'language',
|
||||
)
|
||||
|
||||
|
||||
def setup(**attrs): # noqa: C901
|
||||
"""The gateway to the Distutils: do everything your setup script needs
|
||||
to do, in a highly flexible and user-driven way. Briefly: create a
|
||||
Distribution instance; find and parse config files; parse the command
|
||||
line; run each Distutils command found there, customized by the options
|
||||
supplied to 'setup()' (as keyword arguments), in config files, and on
|
||||
the command line.
|
||||
|
||||
The Distribution instance might be an instance of a class supplied via
|
||||
the 'distclass' keyword argument to 'setup'; if no such class is
|
||||
supplied, then the Distribution class (in dist.py) is instantiated.
|
||||
All other arguments to 'setup' (except for 'cmdclass') are used to set
|
||||
attributes of the Distribution instance.
|
||||
|
||||
The 'cmdclass' argument, if supplied, is a dictionary mapping command
|
||||
names to command classes. Each command encountered on the command line
|
||||
will be turned into a command class, which is in turn instantiated; any
|
||||
class found in 'cmdclass' is used in place of the default, which is
|
||||
(for command 'foo_bar') class 'foo_bar' in module
|
||||
'distutils.command.foo_bar'. The command class must provide a
|
||||
'user_options' attribute which is a list of option specifiers for
|
||||
'distutils.fancy_getopt'. Any command-line options between the current
|
||||
and the next command are used to set attributes of the current command
|
||||
object.
|
||||
|
||||
When the entire command-line has been successfully parsed, calls the
|
||||
'run()' method on each command object in turn. This method will be
|
||||
driven entirely by the Distribution object (which each command object
|
||||
has a reference to, thanks to its constructor), and the
|
||||
command-specific options that became attributes of each command
|
||||
object.
|
||||
"""
|
||||
|
||||
global _setup_stop_after, _setup_distribution
|
||||
|
||||
# Determine the distribution class -- either caller-supplied or
|
||||
# our Distribution (see below).
|
||||
klass = attrs.get('distclass')
|
||||
if klass:
|
||||
attrs.pop('distclass')
|
||||
else:
|
||||
klass = Distribution
|
||||
|
||||
if 'script_name' not in attrs:
|
||||
attrs['script_name'] = os.path.basename(sys.argv[0])
|
||||
if 'script_args' not in attrs:
|
||||
attrs['script_args'] = sys.argv[1:]
|
||||
|
||||
# Create the Distribution instance, using the remaining arguments
|
||||
# (ie. everything except distclass) to initialize it
|
||||
try:
|
||||
_setup_distribution = dist = klass(attrs)
|
||||
except DistutilsSetupError as msg:
|
||||
if 'name' not in attrs:
|
||||
raise SystemExit(f"error in setup command: {msg}")
|
||||
else:
|
||||
raise SystemExit("error in {} setup command: {}".format(attrs['name'], msg))
|
||||
|
||||
if _setup_stop_after == "init":
|
||||
return dist
|
||||
|
||||
# Find and parse the config file(s): they will override options from
|
||||
# the setup script, but be overridden by the command line.
|
||||
dist.parse_config_files()
|
||||
|
||||
if DEBUG:
|
||||
print("options (after parsing config files):")
|
||||
dist.dump_option_dicts()
|
||||
|
||||
if _setup_stop_after == "config":
|
||||
return dist
|
||||
|
||||
# Parse the command line and override config files; any
|
||||
# command-line errors are the end user's fault, so turn them into
|
||||
# SystemExit to suppress tracebacks.
|
||||
try:
|
||||
ok = dist.parse_command_line()
|
||||
except DistutilsArgError as msg:
|
||||
raise SystemExit(gen_usage(dist.script_name) + f"\nerror: {msg}")
|
||||
|
||||
if DEBUG:
|
||||
print("options (after parsing command line):")
|
||||
dist.dump_option_dicts()
|
||||
|
||||
if _setup_stop_after == "commandline":
|
||||
return dist
|
||||
|
||||
# And finally, run all the commands found on the command line.
|
||||
if ok:
|
||||
return run_commands(dist)
|
||||
|
||||
return dist
|
||||
|
||||
|
||||
# setup ()
|
||||
|
||||
|
||||
def run_commands(dist):
|
||||
"""Given a Distribution object run all the commands,
|
||||
raising ``SystemExit`` errors in the case of failure.
|
||||
|
||||
This function assumes that either ``sys.argv`` or ``dist.script_args``
|
||||
is already set accordingly.
|
||||
"""
|
||||
try:
|
||||
dist.run_commands()
|
||||
except KeyboardInterrupt:
|
||||
raise SystemExit("interrupted")
|
||||
except OSError as exc:
|
||||
if DEBUG:
|
||||
sys.stderr.write(f"error: {exc}\n")
|
||||
raise
|
||||
else:
|
||||
raise SystemExit(f"error: {exc}")
|
||||
|
||||
except (DistutilsError, CCompilerError) as msg:
|
||||
if DEBUG:
|
||||
raise
|
||||
else:
|
||||
raise SystemExit("error: " + str(msg))
|
||||
|
||||
return dist
|
||||
|
||||
|
||||
def run_setup(script_name, script_args: Iterable[str] | None = None, stop_after="run"):
|
||||
"""Run a setup script in a somewhat controlled environment, and
|
||||
return the Distribution instance that drives things. This is useful
|
||||
if you need to find out the distribution meta-data (passed as
|
||||
keyword args from 'script' to 'setup()', or the contents of the
|
||||
config files or command-line.
|
||||
|
||||
'script_name' is a file that will be read and run with 'exec()';
|
||||
'sys.argv[0]' will be replaced with 'script' for the duration of the
|
||||
call. 'script_args' is a list of strings; if supplied,
|
||||
'sys.argv[1:]' will be replaced by 'script_args' for the duration of
|
||||
the call.
|
||||
|
||||
'stop_after' tells 'setup()' when to stop processing; possible
|
||||
values:
|
||||
init
|
||||
stop after the Distribution instance has been created and
|
||||
populated with the keyword arguments to 'setup()'
|
||||
config
|
||||
stop after config files have been parsed (and their data
|
||||
stored in the Distribution instance)
|
||||
commandline
|
||||
stop after the command-line ('sys.argv[1:]' or 'script_args')
|
||||
have been parsed (and the data stored in the Distribution)
|
||||
run [default]
|
||||
stop after all commands have been run (the same as if 'setup()'
|
||||
had been called in the usual way
|
||||
|
||||
Returns the Distribution instance, which provides all information
|
||||
used to drive the Distutils.
|
||||
"""
|
||||
if stop_after not in ('init', 'config', 'commandline', 'run'):
|
||||
raise ValueError(f"invalid value for 'stop_after': {stop_after!r}")
|
||||
|
||||
global _setup_stop_after, _setup_distribution
|
||||
_setup_stop_after = stop_after
|
||||
|
||||
save_argv = sys.argv.copy()
|
||||
g = {'__file__': script_name, '__name__': '__main__'}
|
||||
try:
|
||||
try:
|
||||
sys.argv[0] = script_name
|
||||
if script_args is not None:
|
||||
sys.argv[1:] = script_args
|
||||
# tokenize.open supports automatic encoding detection
|
||||
with tokenize.open(script_name) as f:
|
||||
code = f.read().replace(r'\r\n', r'\n')
|
||||
exec(code, g)
|
||||
finally:
|
||||
sys.argv = save_argv
|
||||
_setup_stop_after = None
|
||||
except SystemExit:
|
||||
# Hmm, should we do something if exiting with a non-zero code
|
||||
# (ie. error)?
|
||||
pass
|
||||
|
||||
if _setup_distribution is None:
|
||||
raise RuntimeError(
|
||||
"'distutils.core.setup()' was never called -- "
|
||||
f"perhaps '{script_name}' is not a Distutils setup script?"
|
||||
)
|
||||
|
||||
# I wonder if the setup script's namespace -- g and l -- would be of
|
||||
# any interest to callers?
|
||||
# print "_setup_distribution:", _setup_distribution
|
||||
return _setup_distribution
|
||||
|
||||
|
||||
# run_setup ()
|
||||
@@ -0,0 +1,31 @@
|
||||
from .compilers.C import cygwin
|
||||
from .compilers.C.cygwin import (
|
||||
CONFIG_H_NOTOK,
|
||||
CONFIG_H_OK,
|
||||
CONFIG_H_UNCERTAIN,
|
||||
check_config_h,
|
||||
get_msvcr,
|
||||
is_cygwincc,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'CONFIG_H_NOTOK',
|
||||
'CONFIG_H_OK',
|
||||
'CONFIG_H_UNCERTAIN',
|
||||
'CygwinCCompiler',
|
||||
'Mingw32CCompiler',
|
||||
'check_config_h',
|
||||
'get_msvcr',
|
||||
'is_cygwincc',
|
||||
]
|
||||
|
||||
|
||||
CygwinCCompiler = cygwin.Compiler
|
||||
Mingw32CCompiler = cygwin.MinGW32Compiler
|
||||
|
||||
|
||||
get_versions = None
|
||||
"""
|
||||
A stand-in for the previous get_versions() function to prevent failures
|
||||
when monkeypatched. See pypa/setuptools#2969.
|
||||
"""
|
||||
5
.venv/Lib/site-packages/setuptools/_distutils/debug.py
Normal file
5
.venv/Lib/site-packages/setuptools/_distutils/debug.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
|
||||
# If DISTUTILS_DEBUG is anything other than the empty string, we run in
|
||||
# debug mode.
|
||||
DEBUG = os.environ.get('DISTUTILS_DEBUG')
|
||||
14
.venv/Lib/site-packages/setuptools/_distutils/dep_util.py
Normal file
14
.venv/Lib/site-packages/setuptools/_distutils/dep_util.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import warnings
|
||||
|
||||
from . import _modified
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name not in ['newer', 'newer_group', 'newer_pairwise']:
|
||||
raise AttributeError(name)
|
||||
warnings.warn(
|
||||
"dep_util is Deprecated. Use functions from setuptools instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return getattr(_modified, name)
|
||||
244
.venv/Lib/site-packages/setuptools/_distutils/dir_util.py
Normal file
244
.venv/Lib/site-packages/setuptools/_distutils/dir_util.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""distutils.dir_util
|
||||
|
||||
Utility functions for manipulating directories and directory trees."""
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from . import file_util
|
||||
from ._log import log
|
||||
from .errors import DistutilsFileError, DistutilsInternalError
|
||||
|
||||
|
||||
class SkipRepeatAbsolutePaths(set):
|
||||
"""
|
||||
Cache for mkpath.
|
||||
|
||||
In addition to cheapening redundant calls, eliminates redundant
|
||||
"creating /foo/bar/baz" messages in dry-run mode.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
SkipRepeatAbsolutePaths.instance = self
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
super(cls, cls.instance).clear()
|
||||
|
||||
def wrap(self, func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(path, *args, **kwargs):
|
||||
if path.absolute() in self:
|
||||
return
|
||||
result = func(path, *args, **kwargs)
|
||||
self.add(path.absolute())
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# Python 3.8 compatibility
|
||||
wrapper = SkipRepeatAbsolutePaths().wrap
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
@wrapper
|
||||
def mkpath(name: pathlib.Path, mode=0o777, verbose=True, dry_run=False) -> None:
|
||||
"""Create a directory and any missing ancestor directories.
|
||||
|
||||
If the directory already exists (or if 'name' is the empty string, which
|
||||
means the current directory, which of course exists), then do nothing.
|
||||
Raise DistutilsFileError if unable to create some directory along the way
|
||||
(eg. some sub-path exists, but is a file rather than a directory).
|
||||
If 'verbose' is true, log the directory created.
|
||||
"""
|
||||
if verbose and not name.is_dir():
|
||||
log.info("creating %s", name)
|
||||
|
||||
try:
|
||||
dry_run or name.mkdir(mode=mode, parents=True, exist_ok=True)
|
||||
except OSError as exc:
|
||||
raise DistutilsFileError(f"could not create '{name}': {exc.args[-1]}")
|
||||
|
||||
|
||||
@mkpath.register
|
||||
def _(name: str, *args, **kwargs):
|
||||
return mkpath(pathlib.Path(name), *args, **kwargs)
|
||||
|
||||
|
||||
@mkpath.register
|
||||
def _(name: None, *args, **kwargs):
|
||||
"""
|
||||
Detect a common bug -- name is None.
|
||||
"""
|
||||
raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})")
|
||||
|
||||
|
||||
def create_tree(base_dir, files, mode=0o777, verbose=True, dry_run=False):
|
||||
"""Create all the empty directories under 'base_dir' needed to put 'files'
|
||||
there.
|
||||
|
||||
'base_dir' is just the name of a directory which doesn't necessarily
|
||||
exist yet; 'files' is a list of filenames to be interpreted relative to
|
||||
'base_dir'. 'base_dir' + the directory portion of every file in 'files'
|
||||
will be created if it doesn't already exist. 'mode', 'verbose' and
|
||||
'dry_run' flags are as for 'mkpath()'.
|
||||
"""
|
||||
# First get the list of directories to create
|
||||
need_dir = set(os.path.join(base_dir, os.path.dirname(file)) for file in files)
|
||||
|
||||
# Now create them
|
||||
for dir in sorted(need_dir):
|
||||
mkpath(dir, mode, verbose=verbose, dry_run=dry_run)
|
||||
|
||||
|
||||
def copy_tree(
|
||||
src,
|
||||
dst,
|
||||
preserve_mode=True,
|
||||
preserve_times=True,
|
||||
preserve_symlinks=False,
|
||||
update=False,
|
||||
verbose=True,
|
||||
dry_run=False,
|
||||
):
|
||||
"""Copy an entire directory tree 'src' to a new location 'dst'.
|
||||
|
||||
Both 'src' and 'dst' must be directory names. If 'src' is not a
|
||||
directory, raise DistutilsFileError. If 'dst' does not exist, it is
|
||||
created with 'mkpath()'. The end result of the copy is that every
|
||||
file in 'src' is copied to 'dst', and directories under 'src' are
|
||||
recursively copied to 'dst'. Return the list of files that were
|
||||
copied or might have been copied, using their output name. The
|
||||
return value is unaffected by 'update' or 'dry_run': it is simply
|
||||
the list of all files under 'src', with the names changed to be
|
||||
under 'dst'.
|
||||
|
||||
'preserve_mode' and 'preserve_times' are the same as for
|
||||
'copy_file'; note that they only apply to regular files, not to
|
||||
directories. If 'preserve_symlinks' is true, symlinks will be
|
||||
copied as symlinks (on platforms that support them!); otherwise
|
||||
(the default), the destination of the symlink will be copied.
|
||||
'update' and 'verbose' are the same as for 'copy_file'.
|
||||
"""
|
||||
if not dry_run and not os.path.isdir(src):
|
||||
raise DistutilsFileError(f"cannot copy tree '{src}': not a directory")
|
||||
try:
|
||||
names = os.listdir(src)
|
||||
except OSError as e:
|
||||
if dry_run:
|
||||
names = []
|
||||
else:
|
||||
raise DistutilsFileError(f"error listing files in '{src}': {e.strerror}")
|
||||
|
||||
if not dry_run:
|
||||
mkpath(dst, verbose=verbose)
|
||||
|
||||
copy_one = functools.partial(
|
||||
_copy_one,
|
||||
src=src,
|
||||
dst=dst,
|
||||
preserve_symlinks=preserve_symlinks,
|
||||
verbose=verbose,
|
||||
dry_run=dry_run,
|
||||
preserve_mode=preserve_mode,
|
||||
preserve_times=preserve_times,
|
||||
update=update,
|
||||
)
|
||||
return list(itertools.chain.from_iterable(map(copy_one, names)))
|
||||
|
||||
|
||||
def _copy_one(
|
||||
name,
|
||||
*,
|
||||
src,
|
||||
dst,
|
||||
preserve_symlinks,
|
||||
verbose,
|
||||
dry_run,
|
||||
preserve_mode,
|
||||
preserve_times,
|
||||
update,
|
||||
):
|
||||
src_name = os.path.join(src, name)
|
||||
dst_name = os.path.join(dst, name)
|
||||
|
||||
if name.startswith('.nfs'):
|
||||
# skip NFS rename files
|
||||
return
|
||||
|
||||
if preserve_symlinks and os.path.islink(src_name):
|
||||
link_dest = os.readlink(src_name)
|
||||
if verbose >= 1:
|
||||
log.info("linking %s -> %s", dst_name, link_dest)
|
||||
if not dry_run:
|
||||
os.symlink(link_dest, dst_name)
|
||||
yield dst_name
|
||||
|
||||
elif os.path.isdir(src_name):
|
||||
yield from copy_tree(
|
||||
src_name,
|
||||
dst_name,
|
||||
preserve_mode,
|
||||
preserve_times,
|
||||
preserve_symlinks,
|
||||
update,
|
||||
verbose=verbose,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
else:
|
||||
file_util.copy_file(
|
||||
src_name,
|
||||
dst_name,
|
||||
preserve_mode,
|
||||
preserve_times,
|
||||
update,
|
||||
verbose=verbose,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
yield dst_name
|
||||
|
||||
|
||||
def _build_cmdtuple(path, cmdtuples):
|
||||
"""Helper for remove_tree()."""
|
||||
for f in os.listdir(path):
|
||||
real_f = os.path.join(path, f)
|
||||
if os.path.isdir(real_f) and not os.path.islink(real_f):
|
||||
_build_cmdtuple(real_f, cmdtuples)
|
||||
else:
|
||||
cmdtuples.append((os.remove, real_f))
|
||||
cmdtuples.append((os.rmdir, path))
|
||||
|
||||
|
||||
def remove_tree(directory, verbose=True, dry_run=False):
|
||||
"""Recursively remove an entire directory tree.
|
||||
|
||||
Any errors are ignored (apart from being reported to stdout if 'verbose'
|
||||
is true).
|
||||
"""
|
||||
if verbose >= 1:
|
||||
log.info("removing '%s' (and everything under it)", directory)
|
||||
if dry_run:
|
||||
return
|
||||
cmdtuples = []
|
||||
_build_cmdtuple(directory, cmdtuples)
|
||||
for cmd in cmdtuples:
|
||||
try:
|
||||
cmd[0](cmd[1])
|
||||
# Clear the cache
|
||||
SkipRepeatAbsolutePaths.clear()
|
||||
except OSError as exc:
|
||||
log.warning("error removing %s: %s", directory, exc)
|
||||
|
||||
|
||||
def ensure_relative(path):
|
||||
"""Take the full path 'path', and make it a relative path.
|
||||
|
||||
This is useful to make 'path' the second argument to os.path.join().
|
||||
"""
|
||||
drive, path = os.path.splitdrive(path)
|
||||
if path[0:1] == os.sep:
|
||||
path = drive + path[1:]
|
||||
return path
|
||||
1386
.venv/Lib/site-packages/setuptools/_distutils/dist.py
Normal file
1386
.venv/Lib/site-packages/setuptools/_distutils/dist.py
Normal file
File diff suppressed because it is too large
Load Diff
108
.venv/Lib/site-packages/setuptools/_distutils/errors.py
Normal file
108
.venv/Lib/site-packages/setuptools/_distutils/errors.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Exceptions used by the Distutils modules.
|
||||
|
||||
Distutils modules may raise these or standard exceptions,
|
||||
including :exc:`SystemExit`.
|
||||
"""
|
||||
|
||||
# compiler exceptions aliased for compatibility
|
||||
from .compilers.C.errors import CompileError as CompileError
|
||||
from .compilers.C.errors import Error as _Error
|
||||
from .compilers.C.errors import LibError as LibError
|
||||
from .compilers.C.errors import LinkError as LinkError
|
||||
from .compilers.C.errors import PreprocessError as PreprocessError
|
||||
from .compilers.C.errors import UnknownFileType as _UnknownFileType
|
||||
|
||||
CCompilerError = _Error
|
||||
UnknownFileError = _UnknownFileType
|
||||
|
||||
|
||||
class DistutilsError(Exception):
|
||||
"""The root of all Distutils evil."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsModuleError(DistutilsError):
|
||||
"""Unable to load an expected module, or to find an expected class
|
||||
within some module (in particular, command modules and classes)."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsClassError(DistutilsError):
|
||||
"""Some command class (or possibly distribution class, if anyone
|
||||
feels a need to subclass Distribution) is found not to be holding
|
||||
up its end of the bargain, ie. implementing some part of the
|
||||
"command "interface."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsGetoptError(DistutilsError):
|
||||
"""The option table provided to 'fancy_getopt()' is bogus."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsArgError(DistutilsError):
|
||||
"""Raised by fancy_getopt in response to getopt.error -- ie. an
|
||||
error in the command line usage."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsFileError(DistutilsError):
|
||||
"""Any problems in the filesystem: expected file not found, etc.
|
||||
Typically this is for problems that we detect before OSError
|
||||
could be raised."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsOptionError(DistutilsError):
|
||||
"""Syntactic/semantic errors in command options, such as use of
|
||||
mutually conflicting options, or inconsistent options,
|
||||
badly-spelled values, etc. No distinction is made between option
|
||||
values originating in the setup script, the command line, config
|
||||
files, or what-have-you -- but if we *know* something originated in
|
||||
the setup script, we'll raise DistutilsSetupError instead."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsSetupError(DistutilsError):
|
||||
"""For errors that can be definitely blamed on the setup script,
|
||||
such as invalid keyword arguments to 'setup()'."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsPlatformError(DistutilsError):
|
||||
"""We don't know how to do something on the current platform (but
|
||||
we do know how to do it on some platform) -- eg. trying to compile
|
||||
C files on a platform not supported by a CCompiler subclass."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsExecError(DistutilsError):
|
||||
"""Any problems executing an external program (such as the C
|
||||
compiler, when compiling C files)."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsInternalError(DistutilsError):
|
||||
"""Internal inconsistencies or impossibilities (obviously, this
|
||||
should never be seen if the code is working!)."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DistutilsTemplateError(DistutilsError):
|
||||
"""Syntax error in a file list template."""
|
||||
|
||||
|
||||
class DistutilsByteCompileError(DistutilsError):
|
||||
"""Byte compile error."""
|
||||
258
.venv/Lib/site-packages/setuptools/_distutils/extension.py
Normal file
258
.venv/Lib/site-packages/setuptools/_distutils/extension.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""distutils.extension
|
||||
|
||||
Provides the Extension class, used to describe C/C++ extension
|
||||
modules in setup scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
|
||||
# This class is really only used by the "build_ext" command, so it might
|
||||
# make sense to put it in distutils.command.build_ext. However, that
|
||||
# module is already big enough, and I want to make this class a bit more
|
||||
# complex to simplify some common cases ("foo" module in "foo.c") and do
|
||||
# better error-checking ("foo.c" actually exists).
|
||||
#
|
||||
# Also, putting this in build_ext.py means every setup script would have to
|
||||
# import that large-ish module (indirectly, through distutils.core) in
|
||||
# order to do anything.
|
||||
|
||||
|
||||
class Extension:
|
||||
"""Just a collection of attributes that describes an extension
|
||||
module and everything needed to build it (hopefully in a portable
|
||||
way, but there are hooks that let you be as unportable as you need).
|
||||
|
||||
Instance attributes:
|
||||
name : string
|
||||
the full name of the extension, including any packages -- ie.
|
||||
*not* a filename or pathname, but Python dotted name
|
||||
sources : Iterable[string | os.PathLike]
|
||||
iterable of source filenames (except strings, which could be misinterpreted
|
||||
as a single filename), relative to the distribution root (where the setup
|
||||
script lives), in Unix form (slash-separated) for portability. Can be any
|
||||
non-string iterable (list, tuple, set, etc.) containing strings or
|
||||
PathLike objects. Source files may be C, C++, SWIG (.i), platform-specific
|
||||
resource files, or whatever else is recognized by the "build_ext" command
|
||||
as source for a Python extension.
|
||||
include_dirs : [string]
|
||||
list of directories to search for C/C++ header files (in Unix
|
||||
form for portability)
|
||||
define_macros : [(name : string, value : string|None)]
|
||||
list of macros to define; each macro is defined using a 2-tuple,
|
||||
where 'value' is either the string to define it to or None to
|
||||
define it without a particular value (equivalent of "#define
|
||||
FOO" in source or -DFOO on Unix C compiler command line)
|
||||
undef_macros : [string]
|
||||
list of macros to undefine explicitly
|
||||
library_dirs : [string]
|
||||
list of directories to search for C/C++ libraries at link time
|
||||
libraries : [string]
|
||||
list of library names (not filenames or paths) to link against
|
||||
runtime_library_dirs : [string]
|
||||
list of directories to search for C/C++ libraries at run time
|
||||
(for shared extensions, this is when the extension is loaded)
|
||||
extra_objects : [string]
|
||||
list of extra files to link with (eg. object files not implied
|
||||
by 'sources', static library that must be explicitly specified,
|
||||
binary resource files, etc.)
|
||||
extra_compile_args : [string]
|
||||
any extra platform- and compiler-specific information to use
|
||||
when compiling the source files in 'sources'. For platforms and
|
||||
compilers where "command line" makes sense, this is typically a
|
||||
list of command-line arguments, but for other platforms it could
|
||||
be anything.
|
||||
extra_link_args : [string]
|
||||
any extra platform- and compiler-specific information to use
|
||||
when linking object files together to create the extension (or
|
||||
to create a new static Python interpreter). Similar
|
||||
interpretation as for 'extra_compile_args'.
|
||||
export_symbols : [string]
|
||||
list of symbols to be exported from a shared extension. Not
|
||||
used on all platforms, and not generally necessary for Python
|
||||
extensions, which typically export exactly one symbol: "init" +
|
||||
extension_name.
|
||||
swig_opts : [string]
|
||||
any extra options to pass to SWIG if a source file has the .i
|
||||
extension.
|
||||
depends : [string]
|
||||
list of files that the extension depends on
|
||||
language : string
|
||||
extension language (i.e. "c", "c++", "objc"). Will be detected
|
||||
from the source extensions if not provided.
|
||||
optional : boolean
|
||||
specifies that a build failure in the extension should not abort the
|
||||
build process, but simply not install the failing extension.
|
||||
"""
|
||||
|
||||
# When adding arguments to this constructor, be sure to update
|
||||
# setup_keywords in core.py.
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
sources: Iterable[str | os.PathLike[str]],
|
||||
include_dirs: list[str] | None = None,
|
||||
define_macros: list[tuple[str, str | None]] | None = None,
|
||||
undef_macros: list[str] | None = None,
|
||||
library_dirs: list[str] | None = None,
|
||||
libraries: list[str] | None = None,
|
||||
runtime_library_dirs: list[str] | None = None,
|
||||
extra_objects: list[str] | None = None,
|
||||
extra_compile_args: list[str] | None = None,
|
||||
extra_link_args: list[str] | None = None,
|
||||
export_symbols: list[str] | None = None,
|
||||
swig_opts: list[str] | None = None,
|
||||
depends: list[str] | None = None,
|
||||
language: str | None = None,
|
||||
optional: bool | None = None,
|
||||
**kw, # To catch unknown keywords
|
||||
):
|
||||
if not isinstance(name, str):
|
||||
raise TypeError("'name' must be a string")
|
||||
|
||||
# handle the string case first; since strings are iterable, disallow them
|
||||
if isinstance(sources, str):
|
||||
raise TypeError(
|
||||
"'sources' must be an iterable of strings or PathLike objects, not a string"
|
||||
)
|
||||
|
||||
# now we check if it's iterable and contains valid types
|
||||
try:
|
||||
self.sources = list(map(os.fspath, sources))
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
"'sources' must be an iterable of strings or PathLike objects"
|
||||
)
|
||||
|
||||
self.name = name
|
||||
self.include_dirs = include_dirs or []
|
||||
self.define_macros = define_macros or []
|
||||
self.undef_macros = undef_macros or []
|
||||
self.library_dirs = library_dirs or []
|
||||
self.libraries = libraries or []
|
||||
self.runtime_library_dirs = runtime_library_dirs or []
|
||||
self.extra_objects = extra_objects or []
|
||||
self.extra_compile_args = extra_compile_args or []
|
||||
self.extra_link_args = extra_link_args or []
|
||||
self.export_symbols = export_symbols or []
|
||||
self.swig_opts = swig_opts or []
|
||||
self.depends = depends or []
|
||||
self.language = language
|
||||
self.optional = optional
|
||||
|
||||
# If there are unknown keyword options, warn about them
|
||||
if len(kw) > 0:
|
||||
options = [repr(option) for option in kw]
|
||||
options = ', '.join(sorted(options))
|
||||
msg = f"Unknown Extension options: {options}"
|
||||
warnings.warn(msg)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<{self.__class__.__module__}.{self.__class__.__qualname__}({self.name!r}) at {id(self):#x}>'
|
||||
|
||||
|
||||
def read_setup_file(filename): # noqa: C901
|
||||
"""Reads a Setup file and returns Extension instances."""
|
||||
from distutils.sysconfig import _variable_rx, expand_makefile_vars, parse_makefile
|
||||
from distutils.text_file import TextFile
|
||||
from distutils.util import split_quoted
|
||||
|
||||
# First pass over the file to gather "VAR = VALUE" assignments.
|
||||
vars = parse_makefile(filename)
|
||||
|
||||
# Second pass to gobble up the real content: lines of the form
|
||||
# <module> ... [<sourcefile> ...] [<cpparg> ...] [<library> ...]
|
||||
file = TextFile(
|
||||
filename,
|
||||
strip_comments=True,
|
||||
skip_blanks=True,
|
||||
join_lines=True,
|
||||
lstrip_ws=True,
|
||||
rstrip_ws=True,
|
||||
)
|
||||
try:
|
||||
extensions = []
|
||||
|
||||
while True:
|
||||
line = file.readline()
|
||||
if line is None: # eof
|
||||
break
|
||||
if _variable_rx.match(line): # VAR=VALUE, handled in first pass
|
||||
continue
|
||||
|
||||
if line[0] == line[-1] == "*":
|
||||
file.warn(f"'{line}' lines not handled yet")
|
||||
continue
|
||||
|
||||
line = expand_makefile_vars(line, vars)
|
||||
words = split_quoted(line)
|
||||
|
||||
# NB. this parses a slightly different syntax than the old
|
||||
# makesetup script: here, there must be exactly one extension per
|
||||
# line, and it must be the first word of the line. I have no idea
|
||||
# why the old syntax supported multiple extensions per line, as
|
||||
# they all wind up being the same.
|
||||
|
||||
module = words[0]
|
||||
ext = Extension(module, [])
|
||||
append_next_word = None
|
||||
|
||||
for word in words[1:]:
|
||||
if append_next_word is not None:
|
||||
append_next_word.append(word)
|
||||
append_next_word = None
|
||||
continue
|
||||
|
||||
suffix = os.path.splitext(word)[1]
|
||||
switch = word[0:2]
|
||||
value = word[2:]
|
||||
|
||||
if suffix in (".c", ".cc", ".cpp", ".cxx", ".c++", ".m", ".mm"):
|
||||
# hmm, should we do something about C vs. C++ sources?
|
||||
# or leave it up to the CCompiler implementation to
|
||||
# worry about?
|
||||
ext.sources.append(word)
|
||||
elif switch == "-I":
|
||||
ext.include_dirs.append(value)
|
||||
elif switch == "-D":
|
||||
equals = value.find("=")
|
||||
if equals == -1: # bare "-DFOO" -- no value
|
||||
ext.define_macros.append((value, None))
|
||||
else: # "-DFOO=blah"
|
||||
ext.define_macros.append((value[0:equals], value[equals + 2 :]))
|
||||
elif switch == "-U":
|
||||
ext.undef_macros.append(value)
|
||||
elif switch == "-C": # only here 'cause makesetup has it!
|
||||
ext.extra_compile_args.append(word)
|
||||
elif switch == "-l":
|
||||
ext.libraries.append(value)
|
||||
elif switch == "-L":
|
||||
ext.library_dirs.append(value)
|
||||
elif switch == "-R":
|
||||
ext.runtime_library_dirs.append(value)
|
||||
elif word == "-rpath":
|
||||
append_next_word = ext.runtime_library_dirs
|
||||
elif word == "-Xlinker":
|
||||
append_next_word = ext.extra_link_args
|
||||
elif word == "-Xcompiler":
|
||||
append_next_word = ext.extra_compile_args
|
||||
elif switch == "-u":
|
||||
ext.extra_link_args.append(word)
|
||||
if not value:
|
||||
append_next_word = ext.extra_link_args
|
||||
elif suffix in (".a", ".so", ".sl", ".o", ".dylib"):
|
||||
# NB. a really faithful emulation of makesetup would
|
||||
# append a .o file to extra_objects only if it
|
||||
# had a slash in it; otherwise, it would s/.o/.c/
|
||||
# and append it to sources. Hmmmm.
|
||||
ext.extra_objects.append(word)
|
||||
else:
|
||||
file.warn(f"unrecognized argument '{word}'")
|
||||
|
||||
extensions.append(ext)
|
||||
finally:
|
||||
file.close()
|
||||
|
||||
return extensions
|
||||
471
.venv/Lib/site-packages/setuptools/_distutils/fancy_getopt.py
Normal file
471
.venv/Lib/site-packages/setuptools/_distutils/fancy_getopt.py
Normal file
@@ -0,0 +1,471 @@
|
||||
"""distutils.fancy_getopt
|
||||
|
||||
Wrapper around the standard getopt module that provides the following
|
||||
additional features:
|
||||
* short and long options are tied together
|
||||
* options have help strings, so fancy_getopt could potentially
|
||||
create a complete usage summary
|
||||
* options set attributes of a passed-in object
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import getopt
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from .errors import DistutilsArgError, DistutilsGetoptError
|
||||
|
||||
# Much like command_re in distutils.core, this is close to but not quite
|
||||
# the same as a Python NAME -- except, in the spirit of most GNU
|
||||
# utilities, we use '-' in place of '_'. (The spirit of LISP lives on!)
|
||||
# The similarities to NAME are again not a coincidence...
|
||||
longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)'
|
||||
longopt_re = re.compile(rf'^{longopt_pat}$')
|
||||
|
||||
# For recognizing "negative alias" options, eg. "quiet=!verbose"
|
||||
neg_alias_re = re.compile(f"^({longopt_pat})=!({longopt_pat})$")
|
||||
|
||||
# This is used to translate long options to legitimate Python identifiers
|
||||
# (for use as attributes of some object).
|
||||
longopt_xlate = str.maketrans('-', '_')
|
||||
|
||||
|
||||
class FancyGetopt:
|
||||
"""Wrapper around the standard 'getopt()' module that provides some
|
||||
handy extra functionality:
|
||||
* short and long options are tied together
|
||||
* options have help strings, and help text can be assembled
|
||||
from them
|
||||
* options set attributes of a passed-in object
|
||||
* boolean options can have "negative aliases" -- eg. if
|
||||
--quiet is the "negative alias" of --verbose, then "--quiet"
|
||||
on the command line sets 'verbose' to false
|
||||
"""
|
||||
|
||||
def __init__(self, option_table=None):
|
||||
# The option table is (currently) a list of tuples. The
|
||||
# tuples may have 3 or four values:
|
||||
# (long_option, short_option, help_string [, repeatable])
|
||||
# if an option takes an argument, its long_option should have '='
|
||||
# appended; short_option should just be a single character, no ':'
|
||||
# in any case. If a long_option doesn't have a corresponding
|
||||
# short_option, short_option should be None. All option tuples
|
||||
# must have long options.
|
||||
self.option_table = option_table
|
||||
|
||||
# 'option_index' maps long option names to entries in the option
|
||||
# table (ie. those 3-tuples).
|
||||
self.option_index = {}
|
||||
if self.option_table:
|
||||
self._build_index()
|
||||
|
||||
# 'alias' records (duh) alias options; {'foo': 'bar'} means
|
||||
# --foo is an alias for --bar
|
||||
self.alias = {}
|
||||
|
||||
# 'negative_alias' keeps track of options that are the boolean
|
||||
# opposite of some other option
|
||||
self.negative_alias = {}
|
||||
|
||||
# These keep track of the information in the option table. We
|
||||
# don't actually populate these structures until we're ready to
|
||||
# parse the command-line, since the 'option_table' passed in here
|
||||
# isn't necessarily the final word.
|
||||
self.short_opts = []
|
||||
self.long_opts = []
|
||||
self.short2long = {}
|
||||
self.attr_name = {}
|
||||
self.takes_arg = {}
|
||||
|
||||
# And 'option_order' is filled up in 'getopt()'; it records the
|
||||
# original order of options (and their values) on the command-line,
|
||||
# but expands short options, converts aliases, etc.
|
||||
self.option_order = []
|
||||
|
||||
def _build_index(self):
|
||||
self.option_index.clear()
|
||||
for option in self.option_table:
|
||||
self.option_index[option[0]] = option
|
||||
|
||||
def set_option_table(self, option_table):
|
||||
self.option_table = option_table
|
||||
self._build_index()
|
||||
|
||||
def add_option(self, long_option, short_option=None, help_string=None):
|
||||
if long_option in self.option_index:
|
||||
raise DistutilsGetoptError(
|
||||
f"option conflict: already an option '{long_option}'"
|
||||
)
|
||||
else:
|
||||
option = (long_option, short_option, help_string)
|
||||
self.option_table.append(option)
|
||||
self.option_index[long_option] = option
|
||||
|
||||
def has_option(self, long_option):
|
||||
"""Return true if the option table for this parser has an
|
||||
option with long name 'long_option'."""
|
||||
return long_option in self.option_index
|
||||
|
||||
def get_attr_name(self, long_option):
|
||||
"""Translate long option name 'long_option' to the form it
|
||||
has as an attribute of some object: ie., translate hyphens
|
||||
to underscores."""
|
||||
return long_option.translate(longopt_xlate)
|
||||
|
||||
def _check_alias_dict(self, aliases, what):
|
||||
assert isinstance(aliases, dict)
|
||||
for alias, opt in aliases.items():
|
||||
if alias not in self.option_index:
|
||||
raise DistutilsGetoptError(
|
||||
f"invalid {what} '{alias}': option '{alias}' not defined"
|
||||
)
|
||||
if opt not in self.option_index:
|
||||
raise DistutilsGetoptError(
|
||||
f"invalid {what} '{alias}': aliased option '{opt}' not defined"
|
||||
)
|
||||
|
||||
def set_aliases(self, alias):
|
||||
"""Set the aliases for this option parser."""
|
||||
self._check_alias_dict(alias, "alias")
|
||||
self.alias = alias
|
||||
|
||||
def set_negative_aliases(self, negative_alias):
|
||||
"""Set the negative aliases for this option parser.
|
||||
'negative_alias' should be a dictionary mapping option names to
|
||||
option names, both the key and value must already be defined
|
||||
in the option table."""
|
||||
self._check_alias_dict(negative_alias, "negative alias")
|
||||
self.negative_alias = negative_alias
|
||||
|
||||
def _grok_option_table(self): # noqa: C901
|
||||
"""Populate the various data structures that keep tabs on the
|
||||
option table. Called by 'getopt()' before it can do anything
|
||||
worthwhile.
|
||||
"""
|
||||
self.long_opts = []
|
||||
self.short_opts = []
|
||||
self.short2long.clear()
|
||||
self.repeat = {}
|
||||
|
||||
for option in self.option_table:
|
||||
if len(option) == 3:
|
||||
long, short, help = option
|
||||
repeat = 0
|
||||
elif len(option) == 4:
|
||||
long, short, help, repeat = option
|
||||
else:
|
||||
# the option table is part of the code, so simply
|
||||
# assert that it is correct
|
||||
raise ValueError(f"invalid option tuple: {option!r}")
|
||||
|
||||
# Type- and value-check the option names
|
||||
if not isinstance(long, str) or len(long) < 2:
|
||||
raise DistutilsGetoptError(
|
||||
f"invalid long option '{long}': must be a string of length >= 2"
|
||||
)
|
||||
|
||||
if not ((short is None) or (isinstance(short, str) and len(short) == 1)):
|
||||
raise DistutilsGetoptError(
|
||||
f"invalid short option '{short}': must a single character or None"
|
||||
)
|
||||
|
||||
self.repeat[long] = repeat
|
||||
self.long_opts.append(long)
|
||||
|
||||
if long[-1] == '=': # option takes an argument?
|
||||
if short:
|
||||
short = short + ':'
|
||||
long = long[0:-1]
|
||||
self.takes_arg[long] = True
|
||||
else:
|
||||
# Is option is a "negative alias" for some other option (eg.
|
||||
# "quiet" == "!verbose")?
|
||||
alias_to = self.negative_alias.get(long)
|
||||
if alias_to is not None:
|
||||
if self.takes_arg[alias_to]:
|
||||
raise DistutilsGetoptError(
|
||||
f"invalid negative alias '{long}': "
|
||||
f"aliased option '{alias_to}' takes a value"
|
||||
)
|
||||
|
||||
self.long_opts[-1] = long # XXX redundant?!
|
||||
self.takes_arg[long] = False
|
||||
|
||||
# If this is an alias option, make sure its "takes arg" flag is
|
||||
# the same as the option it's aliased to.
|
||||
alias_to = self.alias.get(long)
|
||||
if alias_to is not None:
|
||||
if self.takes_arg[long] != self.takes_arg[alias_to]:
|
||||
raise DistutilsGetoptError(
|
||||
f"invalid alias '{long}': inconsistent with "
|
||||
f"aliased option '{alias_to}' (one of them takes a value, "
|
||||
"the other doesn't"
|
||||
)
|
||||
|
||||
# Now enforce some bondage on the long option name, so we can
|
||||
# later translate it to an attribute name on some object. Have
|
||||
# to do this a bit late to make sure we've removed any trailing
|
||||
# '='.
|
||||
if not longopt_re.match(long):
|
||||
raise DistutilsGetoptError(
|
||||
f"invalid long option name '{long}' "
|
||||
"(must be letters, numbers, hyphens only"
|
||||
)
|
||||
|
||||
self.attr_name[long] = self.get_attr_name(long)
|
||||
if short:
|
||||
self.short_opts.append(short)
|
||||
self.short2long[short[0]] = long
|
||||
|
||||
def getopt(self, args: Sequence[str] | None = None, object=None): # noqa: C901
|
||||
"""Parse command-line options in args. Store as attributes on object.
|
||||
|
||||
If 'args' is None or not supplied, uses 'sys.argv[1:]'. If
|
||||
'object' is None or not supplied, creates a new OptionDummy
|
||||
object, stores option values there, and returns a tuple (args,
|
||||
object). If 'object' is supplied, it is modified in place and
|
||||
'getopt()' just returns 'args'; in both cases, the returned
|
||||
'args' is a modified copy of the passed-in 'args' list, which
|
||||
is left untouched.
|
||||
"""
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
if object is None:
|
||||
object = OptionDummy()
|
||||
created_object = True
|
||||
else:
|
||||
created_object = False
|
||||
|
||||
self._grok_option_table()
|
||||
|
||||
short_opts = ' '.join(self.short_opts)
|
||||
try:
|
||||
opts, args = getopt.getopt(args, short_opts, self.long_opts)
|
||||
except getopt.error as msg:
|
||||
raise DistutilsArgError(msg)
|
||||
|
||||
for opt, val in opts:
|
||||
if len(opt) == 2 and opt[0] == '-': # it's a short option
|
||||
opt = self.short2long[opt[1]]
|
||||
else:
|
||||
assert len(opt) > 2 and opt[:2] == '--'
|
||||
opt = opt[2:]
|
||||
|
||||
alias = self.alias.get(opt)
|
||||
if alias:
|
||||
opt = alias
|
||||
|
||||
if not self.takes_arg[opt]: # boolean option?
|
||||
assert val == '', "boolean option can't have value"
|
||||
alias = self.negative_alias.get(opt)
|
||||
if alias:
|
||||
opt = alias
|
||||
val = 0
|
||||
else:
|
||||
val = 1
|
||||
|
||||
attr = self.attr_name[opt]
|
||||
# The only repeating option at the moment is 'verbose'.
|
||||
# It has a negative option -q quiet, which should set verbose = False.
|
||||
if val and self.repeat.get(attr) is not None:
|
||||
val = getattr(object, attr, 0) + 1
|
||||
setattr(object, attr, val)
|
||||
self.option_order.append((opt, val))
|
||||
|
||||
# for opts
|
||||
if created_object:
|
||||
return args, object
|
||||
else:
|
||||
return args
|
||||
|
||||
def get_option_order(self):
|
||||
"""Returns the list of (option, value) tuples processed by the
|
||||
previous run of 'getopt()'. Raises RuntimeError if
|
||||
'getopt()' hasn't been called yet.
|
||||
"""
|
||||
if self.option_order is None:
|
||||
raise RuntimeError("'getopt()' hasn't been called yet")
|
||||
else:
|
||||
return self.option_order
|
||||
|
||||
def generate_help(self, header=None): # noqa: C901
|
||||
"""Generate help text (a list of strings, one per suggested line of
|
||||
output) from the option table for this FancyGetopt object.
|
||||
"""
|
||||
# Blithely assume the option table is good: probably wouldn't call
|
||||
# 'generate_help()' unless you've already called 'getopt()'.
|
||||
|
||||
# First pass: determine maximum length of long option names
|
||||
max_opt = 0
|
||||
for option in self.option_table:
|
||||
long = option[0]
|
||||
short = option[1]
|
||||
ell = len(long)
|
||||
if long[-1] == '=':
|
||||
ell = ell - 1
|
||||
if short is not None:
|
||||
ell = ell + 5 # " (-x)" where short == 'x'
|
||||
if ell > max_opt:
|
||||
max_opt = ell
|
||||
|
||||
opt_width = max_opt + 2 + 2 + 2 # room for indent + dashes + gutter
|
||||
|
||||
# Typical help block looks like this:
|
||||
# --foo controls foonabulation
|
||||
# Help block for longest option looks like this:
|
||||
# --flimflam set the flim-flam level
|
||||
# and with wrapped text:
|
||||
# --flimflam set the flim-flam level (must be between
|
||||
# 0 and 100, except on Tuesdays)
|
||||
# Options with short names will have the short name shown (but
|
||||
# it doesn't contribute to max_opt):
|
||||
# --foo (-f) controls foonabulation
|
||||
# If adding the short option would make the left column too wide,
|
||||
# we push the explanation off to the next line
|
||||
# --flimflam (-l)
|
||||
# set the flim-flam level
|
||||
# Important parameters:
|
||||
# - 2 spaces before option block start lines
|
||||
# - 2 dashes for each long option name
|
||||
# - min. 2 spaces between option and explanation (gutter)
|
||||
# - 5 characters (incl. space) for short option name
|
||||
|
||||
# Now generate lines of help text. (If 80 columns were good enough
|
||||
# for Jesus, then 78 columns are good enough for me!)
|
||||
line_width = 78
|
||||
text_width = line_width - opt_width
|
||||
big_indent = ' ' * opt_width
|
||||
if header:
|
||||
lines = [header]
|
||||
else:
|
||||
lines = ['Option summary:']
|
||||
|
||||
for option in self.option_table:
|
||||
long, short, help = option[:3]
|
||||
text = wrap_text(help, text_width)
|
||||
if long[-1] == '=':
|
||||
long = long[0:-1]
|
||||
|
||||
# Case 1: no short option at all (makes life easy)
|
||||
if short is None:
|
||||
if text:
|
||||
lines.append(f" --{long:<{max_opt}} {text[0]}")
|
||||
else:
|
||||
lines.append(f" --{long:<{max_opt}}")
|
||||
|
||||
# Case 2: we have a short option, so we have to include it
|
||||
# just after the long option
|
||||
else:
|
||||
opt_names = f"{long} (-{short})"
|
||||
if text:
|
||||
lines.append(f" --{opt_names:<{max_opt}} {text[0]}")
|
||||
else:
|
||||
lines.append(f" --{opt_names:<{max_opt}}")
|
||||
|
||||
for ell in text[1:]:
|
||||
lines.append(big_indent + ell)
|
||||
return lines
|
||||
|
||||
def print_help(self, header=None, file=None):
|
||||
if file is None:
|
||||
file = sys.stdout
|
||||
for line in self.generate_help(header):
|
||||
file.write(line + "\n")
|
||||
|
||||
|
||||
def fancy_getopt(options, negative_opt, object, args: Sequence[str] | None):
|
||||
parser = FancyGetopt(options)
|
||||
parser.set_negative_aliases(negative_opt)
|
||||
return parser.getopt(args, object)
|
||||
|
||||
|
||||
WS_TRANS = {ord(_wschar): ' ' for _wschar in string.whitespace}
|
||||
|
||||
|
||||
def wrap_text(text, width):
|
||||
"""wrap_text(text : string, width : int) -> [string]
|
||||
|
||||
Split 'text' into multiple lines of no more than 'width' characters
|
||||
each, and return the list of strings that results.
|
||||
"""
|
||||
if text is None:
|
||||
return []
|
||||
if len(text) <= width:
|
||||
return [text]
|
||||
|
||||
text = text.expandtabs()
|
||||
text = text.translate(WS_TRANS)
|
||||
chunks = re.split(r'( +|-+)', text)
|
||||
chunks = [ch for ch in chunks if ch] # ' - ' results in empty strings
|
||||
lines = []
|
||||
|
||||
while chunks:
|
||||
cur_line = [] # list of chunks (to-be-joined)
|
||||
cur_len = 0 # length of current line
|
||||
|
||||
while chunks:
|
||||
ell = len(chunks[0])
|
||||
if cur_len + ell <= width: # can squeeze (at least) this chunk in
|
||||
cur_line.append(chunks[0])
|
||||
del chunks[0]
|
||||
cur_len = cur_len + ell
|
||||
else: # this line is full
|
||||
# drop last chunk if all space
|
||||
if cur_line and cur_line[-1][0] == ' ':
|
||||
del cur_line[-1]
|
||||
break
|
||||
|
||||
if chunks: # any chunks left to process?
|
||||
# if the current line is still empty, then we had a single
|
||||
# chunk that's too big too fit on a line -- so we break
|
||||
# down and break it up at the line width
|
||||
if cur_len == 0:
|
||||
cur_line.append(chunks[0][0:width])
|
||||
chunks[0] = chunks[0][width:]
|
||||
|
||||
# all-whitespace chunks at the end of a line can be discarded
|
||||
# (and we know from the re.split above that if a chunk has
|
||||
# *any* whitespace, it is *all* whitespace)
|
||||
if chunks[0][0] == ' ':
|
||||
del chunks[0]
|
||||
|
||||
# and store this line in the list-of-all-lines -- as a single
|
||||
# string, of course!
|
||||
lines.append(''.join(cur_line))
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def translate_longopt(opt):
|
||||
"""Convert a long option name to a valid Python identifier by
|
||||
changing "-" to "_".
|
||||
"""
|
||||
return opt.translate(longopt_xlate)
|
||||
|
||||
|
||||
class OptionDummy:
|
||||
"""Dummy class just used as a place to hold command-line option
|
||||
values as instance attributes."""
|
||||
|
||||
def __init__(self, options: Sequence[Any] = []):
|
||||
"""Create a new OptionDummy instance. The attributes listed in
|
||||
'options' will be initialized to None."""
|
||||
for opt in options:
|
||||
setattr(self, opt, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
text = """\
|
||||
Tra-la-la, supercalifragilisticexpialidocious.
|
||||
How *do* you spell that odd word, anyways?
|
||||
(Someone ask Mary -- she'll know [or she'll
|
||||
say, "How should I know?"].)"""
|
||||
|
||||
for w in (10, 20, 30, 40):
|
||||
print(f"width: {w}")
|
||||
print("\n".join(wrap_text(text, w)))
|
||||
print()
|
||||
236
.venv/Lib/site-packages/setuptools/_distutils/file_util.py
Normal file
236
.venv/Lib/site-packages/setuptools/_distutils/file_util.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""distutils.file_util
|
||||
|
||||
Utility functions for operating on single files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ._log import log
|
||||
from .errors import DistutilsFileError
|
||||
|
||||
# for generating verbose output in 'copy_file()'
|
||||
_copy_action = {None: 'copying', 'hard': 'hard linking', 'sym': 'symbolically linking'}
|
||||
|
||||
|
||||
def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C901
|
||||
"""Copy the file 'src' to 'dst'; both must be filenames. Any error
|
||||
opening either file, reading from 'src', or writing to 'dst', raises
|
||||
DistutilsFileError. Data is read/written in chunks of 'buffer_size'
|
||||
bytes (default 16k). No attempt is made to handle anything apart from
|
||||
regular files.
|
||||
"""
|
||||
# Stolen from shutil module in the standard library, but with
|
||||
# custom error-handling added.
|
||||
fsrc = None
|
||||
fdst = None
|
||||
try:
|
||||
try:
|
||||
fsrc = open(src, 'rb')
|
||||
except OSError as e:
|
||||
raise DistutilsFileError(f"could not open '{src}': {e.strerror}")
|
||||
|
||||
if os.path.exists(dst):
|
||||
try:
|
||||
os.unlink(dst)
|
||||
except OSError as e:
|
||||
raise DistutilsFileError(f"could not delete '{dst}': {e.strerror}")
|
||||
|
||||
try:
|
||||
fdst = open(dst, 'wb')
|
||||
except OSError as e:
|
||||
raise DistutilsFileError(f"could not create '{dst}': {e.strerror}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
buf = fsrc.read(buffer_size)
|
||||
except OSError as e:
|
||||
raise DistutilsFileError(f"could not read from '{src}': {e.strerror}")
|
||||
|
||||
if not buf:
|
||||
break
|
||||
|
||||
try:
|
||||
fdst.write(buf)
|
||||
except OSError as e:
|
||||
raise DistutilsFileError(f"could not write to '{dst}': {e.strerror}")
|
||||
finally:
|
||||
if fdst:
|
||||
fdst.close()
|
||||
if fsrc:
|
||||
fsrc.close()
|
||||
|
||||
|
||||
def copy_file( # noqa: C901
|
||||
src,
|
||||
dst,
|
||||
preserve_mode=True,
|
||||
preserve_times=True,
|
||||
update=False,
|
||||
link=None,
|
||||
verbose=True,
|
||||
dry_run=False,
|
||||
):
|
||||
"""Copy a file 'src' to 'dst'. If 'dst' is a directory, then 'src' is
|
||||
copied there with the same name; otherwise, it must be a filename. (If
|
||||
the file exists, it will be ruthlessly clobbered.) If 'preserve_mode'
|
||||
is true (the default), the file's mode (type and permission bits, or
|
||||
whatever is analogous on the current platform) is copied. If
|
||||
'preserve_times' is true (the default), the last-modified and
|
||||
last-access times are copied as well. If 'update' is true, 'src' will
|
||||
only be copied if 'dst' does not exist, or if 'dst' does exist but is
|
||||
older than 'src'.
|
||||
|
||||
'link' allows you to make hard links (os.link) or symbolic links
|
||||
(os.symlink) instead of copying: set it to "hard" or "sym"; if it is
|
||||
None (the default), files are copied. Don't set 'link' on systems that
|
||||
don't support it: 'copy_file()' doesn't check if hard or symbolic
|
||||
linking is available. If hardlink fails, falls back to
|
||||
_copy_file_contents().
|
||||
|
||||
Under Mac OS, uses the native file copy function in macostools; on
|
||||
other systems, uses '_copy_file_contents()' to copy file contents.
|
||||
|
||||
Return a tuple (dest_name, copied): 'dest_name' is the actual name of
|
||||
the output file, and 'copied' is true if the file was copied (or would
|
||||
have been copied, if 'dry_run' true).
|
||||
"""
|
||||
# XXX if the destination file already exists, we clobber it if
|
||||
# copying, but blow up if linking. Hmmm. And I don't know what
|
||||
# macostools.copyfile() does. Should definitely be consistent, and
|
||||
# should probably blow up if destination exists and we would be
|
||||
# changing it (ie. it's not already a hard/soft link to src OR
|
||||
# (not update) and (src newer than dst).
|
||||
|
||||
from distutils._modified import newer
|
||||
from stat import S_IMODE, ST_ATIME, ST_MODE, ST_MTIME
|
||||
|
||||
if not os.path.isfile(src):
|
||||
raise DistutilsFileError(
|
||||
f"can't copy '{src}': doesn't exist or not a regular file"
|
||||
)
|
||||
|
||||
if os.path.isdir(dst):
|
||||
dir = dst
|
||||
dst = os.path.join(dst, os.path.basename(src))
|
||||
else:
|
||||
dir = os.path.dirname(dst)
|
||||
|
||||
if update and not newer(src, dst):
|
||||
if verbose >= 1:
|
||||
log.debug("not copying %s (output up-to-date)", src)
|
||||
return (dst, False)
|
||||
|
||||
try:
|
||||
action = _copy_action[link]
|
||||
except KeyError:
|
||||
raise ValueError(f"invalid value '{link}' for 'link' argument")
|
||||
|
||||
if verbose >= 1:
|
||||
if os.path.basename(dst) == os.path.basename(src):
|
||||
log.info("%s %s -> %s", action, src, dir)
|
||||
else:
|
||||
log.info("%s %s -> %s", action, src, dst)
|
||||
|
||||
if dry_run:
|
||||
return (dst, True)
|
||||
|
||||
# If linking (hard or symbolic), use the appropriate system call
|
||||
# (Unix only, of course, but that's the caller's responsibility)
|
||||
elif link == 'hard':
|
||||
if not (os.path.exists(dst) and os.path.samefile(src, dst)):
|
||||
try:
|
||||
os.link(src, dst)
|
||||
except OSError:
|
||||
# If hard linking fails, fall back on copying file
|
||||
# (some special filesystems don't support hard linking
|
||||
# even under Unix, see issue #8876).
|
||||
pass
|
||||
else:
|
||||
return (dst, True)
|
||||
elif link == 'sym':
|
||||
if not (os.path.exists(dst) and os.path.samefile(src, dst)):
|
||||
os.symlink(src, dst)
|
||||
return (dst, True)
|
||||
|
||||
# Otherwise (non-Mac, not linking), copy the file contents and
|
||||
# (optionally) copy the times and mode.
|
||||
_copy_file_contents(src, dst)
|
||||
if preserve_mode or preserve_times:
|
||||
st = os.stat(src)
|
||||
|
||||
# According to David Ascher <da@ski.org>, utime() should be done
|
||||
# before chmod() (at least under NT).
|
||||
if preserve_times:
|
||||
os.utime(dst, (st[ST_ATIME], st[ST_MTIME]))
|
||||
if preserve_mode:
|
||||
os.chmod(dst, S_IMODE(st[ST_MODE]))
|
||||
|
||||
return (dst, True)
|
||||
|
||||
|
||||
# XXX I suspect this is Unix-specific -- need porting help!
|
||||
def move_file(src, dst, verbose=True, dry_run=False): # noqa: C901
|
||||
"""Move a file 'src' to 'dst'. If 'dst' is a directory, the file will
|
||||
be moved into it with the same name; otherwise, 'src' is just renamed
|
||||
to 'dst'. Return the new full name of the file.
|
||||
|
||||
Handles cross-device moves on Unix using 'copy_file()'. What about
|
||||
other systems???
|
||||
"""
|
||||
import errno
|
||||
from os.path import basename, dirname, exists, isdir, isfile
|
||||
|
||||
if verbose >= 1:
|
||||
log.info("moving %s -> %s", src, dst)
|
||||
|
||||
if dry_run:
|
||||
return dst
|
||||
|
||||
if not isfile(src):
|
||||
raise DistutilsFileError(f"can't move '{src}': not a regular file")
|
||||
|
||||
if isdir(dst):
|
||||
dst = os.path.join(dst, basename(src))
|
||||
elif exists(dst):
|
||||
raise DistutilsFileError(
|
||||
f"can't move '{src}': destination '{dst}' already exists"
|
||||
)
|
||||
|
||||
if not isdir(dirname(dst)):
|
||||
raise DistutilsFileError(
|
||||
f"can't move '{src}': destination '{dst}' not a valid path"
|
||||
)
|
||||
|
||||
copy_it = False
|
||||
try:
|
||||
os.rename(src, dst)
|
||||
except OSError as e:
|
||||
(num, msg) = e.args
|
||||
if num == errno.EXDEV:
|
||||
copy_it = True
|
||||
else:
|
||||
raise DistutilsFileError(f"couldn't move '{src}' to '{dst}': {msg}")
|
||||
|
||||
if copy_it:
|
||||
copy_file(src, dst, verbose=verbose)
|
||||
try:
|
||||
os.unlink(src)
|
||||
except OSError as e:
|
||||
(num, msg) = e.args
|
||||
try:
|
||||
os.unlink(dst)
|
||||
except OSError:
|
||||
pass
|
||||
raise DistutilsFileError(
|
||||
f"couldn't move '{src}' to '{dst}' by copy/delete: "
|
||||
f"delete '{src}' failed: {msg}"
|
||||
)
|
||||
return dst
|
||||
|
||||
|
||||
def write_file(filename, contents):
|
||||
"""Create a file with the specified name and write 'contents' (a
|
||||
sequence of strings without line terminators) to it.
|
||||
"""
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.writelines(line + '\n' for line in contents)
|
||||
431
.venv/Lib/site-packages/setuptools/_distutils/filelist.py
Normal file
431
.venv/Lib/site-packages/setuptools/_distutils/filelist.py
Normal file
@@ -0,0 +1,431 @@
|
||||
"""distutils.filelist
|
||||
|
||||
Provides the FileList class, used for poking about the filesystem
|
||||
and building lists of files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from typing import Literal, overload
|
||||
|
||||
from ._log import log
|
||||
from .errors import DistutilsInternalError, DistutilsTemplateError
|
||||
from .util import convert_path
|
||||
|
||||
|
||||
class FileList:
|
||||
"""A list of files built by on exploring the filesystem and filtered by
|
||||
applying various patterns to what we find there.
|
||||
|
||||
Instance attributes:
|
||||
dir
|
||||
directory from which files will be taken -- only used if
|
||||
'allfiles' not supplied to constructor
|
||||
files
|
||||
list of filenames currently being built/filtered/manipulated
|
||||
allfiles
|
||||
complete list of files under consideration (ie. without any
|
||||
filtering applied)
|
||||
"""
|
||||
|
||||
def __init__(self, warn: object = None, debug_print: object = None) -> None:
|
||||
# ignore argument to FileList, but keep them for backwards
|
||||
# compatibility
|
||||
self.allfiles: Iterable[str] | None = None
|
||||
self.files: list[str] = []
|
||||
|
||||
def set_allfiles(self, allfiles: Iterable[str]) -> None:
|
||||
self.allfiles = allfiles
|
||||
|
||||
def findall(self, dir: str | os.PathLike[str] = os.curdir) -> None:
|
||||
self.allfiles = findall(dir)
|
||||
|
||||
def debug_print(self, msg: object) -> None:
|
||||
"""Print 'msg' to stdout if the global DEBUG (taken from the
|
||||
DISTUTILS_DEBUG environment variable) flag is true.
|
||||
"""
|
||||
from distutils.debug import DEBUG
|
||||
|
||||
if DEBUG:
|
||||
print(msg)
|
||||
|
||||
# Collection methods
|
||||
|
||||
def append(self, item: str) -> None:
|
||||
self.files.append(item)
|
||||
|
||||
def extend(self, items: Iterable[str]) -> None:
|
||||
self.files.extend(items)
|
||||
|
||||
def sort(self) -> None:
|
||||
# Not a strict lexical sort!
|
||||
sortable_files = sorted(map(os.path.split, self.files))
|
||||
self.files = []
|
||||
for sort_tuple in sortable_files:
|
||||
self.files.append(os.path.join(*sort_tuple))
|
||||
|
||||
# Other miscellaneous utility methods
|
||||
|
||||
def remove_duplicates(self) -> None:
|
||||
# Assumes list has been sorted!
|
||||
for i in range(len(self.files) - 1, 0, -1):
|
||||
if self.files[i] == self.files[i - 1]:
|
||||
del self.files[i]
|
||||
|
||||
# "File template" methods
|
||||
|
||||
def _parse_template_line(self, line):
|
||||
words = line.split()
|
||||
action = words[0]
|
||||
|
||||
patterns = dir = dir_pattern = None
|
||||
|
||||
if action in ('include', 'exclude', 'global-include', 'global-exclude'):
|
||||
if len(words) < 2:
|
||||
raise DistutilsTemplateError(
|
||||
f"'{action}' expects <pattern1> <pattern2> ..."
|
||||
)
|
||||
patterns = [convert_path(w) for w in words[1:]]
|
||||
elif action in ('recursive-include', 'recursive-exclude'):
|
||||
if len(words) < 3:
|
||||
raise DistutilsTemplateError(
|
||||
f"'{action}' expects <dir> <pattern1> <pattern2> ..."
|
||||
)
|
||||
dir = convert_path(words[1])
|
||||
patterns = [convert_path(w) for w in words[2:]]
|
||||
elif action in ('graft', 'prune'):
|
||||
if len(words) != 2:
|
||||
raise DistutilsTemplateError(
|
||||
f"'{action}' expects a single <dir_pattern>"
|
||||
)
|
||||
dir_pattern = convert_path(words[1])
|
||||
else:
|
||||
raise DistutilsTemplateError(f"unknown action '{action}'")
|
||||
|
||||
return (action, patterns, dir, dir_pattern)
|
||||
|
||||
def process_template_line(self, line: str) -> None: # noqa: C901
|
||||
# Parse the line: split it up, make sure the right number of words
|
||||
# is there, and return the relevant words. 'action' is always
|
||||
# defined: it's the first word of the line. Which of the other
|
||||
# three are defined depends on the action; it'll be either
|
||||
# patterns, (dir and patterns), or (dir_pattern).
|
||||
(action, patterns, dir, dir_pattern) = self._parse_template_line(line)
|
||||
|
||||
# OK, now we know that the action is valid and we have the
|
||||
# right number of words on the line for that action -- so we
|
||||
# can proceed with minimal error-checking.
|
||||
if action == 'include':
|
||||
self.debug_print("include " + ' '.join(patterns))
|
||||
for pattern in patterns:
|
||||
if not self.include_pattern(pattern, anchor=True):
|
||||
log.warning("warning: no files found matching '%s'", pattern)
|
||||
|
||||
elif action == 'exclude':
|
||||
self.debug_print("exclude " + ' '.join(patterns))
|
||||
for pattern in patterns:
|
||||
if not self.exclude_pattern(pattern, anchor=True):
|
||||
log.warning(
|
||||
"warning: no previously-included files found matching '%s'",
|
||||
pattern,
|
||||
)
|
||||
|
||||
elif action == 'global-include':
|
||||
self.debug_print("global-include " + ' '.join(patterns))
|
||||
for pattern in patterns:
|
||||
if not self.include_pattern(pattern, anchor=False):
|
||||
log.warning(
|
||||
(
|
||||
"warning: no files found matching '%s' "
|
||||
"anywhere in distribution"
|
||||
),
|
||||
pattern,
|
||||
)
|
||||
|
||||
elif action == 'global-exclude':
|
||||
self.debug_print("global-exclude " + ' '.join(patterns))
|
||||
for pattern in patterns:
|
||||
if not self.exclude_pattern(pattern, anchor=False):
|
||||
log.warning(
|
||||
(
|
||||
"warning: no previously-included files matching "
|
||||
"'%s' found anywhere in distribution"
|
||||
),
|
||||
pattern,
|
||||
)
|
||||
|
||||
elif action == 'recursive-include':
|
||||
self.debug_print("recursive-include {} {}".format(dir, ' '.join(patterns)))
|
||||
for pattern in patterns:
|
||||
if not self.include_pattern(pattern, prefix=dir):
|
||||
msg = "warning: no files found matching '%s' under directory '%s'"
|
||||
log.warning(msg, pattern, dir)
|
||||
|
||||
elif action == 'recursive-exclude':
|
||||
self.debug_print("recursive-exclude {} {}".format(dir, ' '.join(patterns)))
|
||||
for pattern in patterns:
|
||||
if not self.exclude_pattern(pattern, prefix=dir):
|
||||
log.warning(
|
||||
(
|
||||
"warning: no previously-included files matching "
|
||||
"'%s' found under directory '%s'"
|
||||
),
|
||||
pattern,
|
||||
dir,
|
||||
)
|
||||
|
||||
elif action == 'graft':
|
||||
self.debug_print("graft " + dir_pattern)
|
||||
if not self.include_pattern(None, prefix=dir_pattern):
|
||||
log.warning("warning: no directories found matching '%s'", dir_pattern)
|
||||
|
||||
elif action == 'prune':
|
||||
self.debug_print("prune " + dir_pattern)
|
||||
if not self.exclude_pattern(None, prefix=dir_pattern):
|
||||
log.warning(
|
||||
("no previously-included directories found matching '%s'"),
|
||||
dir_pattern,
|
||||
)
|
||||
else:
|
||||
raise DistutilsInternalError(
|
||||
f"this cannot happen: invalid action '{action}'"
|
||||
)
|
||||
|
||||
# Filtering/selection methods
|
||||
@overload
|
||||
def include_pattern(
|
||||
self,
|
||||
pattern: str,
|
||||
anchor: bool = True,
|
||||
prefix: str | None = None,
|
||||
is_regex: Literal[False] = False,
|
||||
) -> bool: ...
|
||||
@overload
|
||||
def include_pattern(
|
||||
self,
|
||||
pattern: str | re.Pattern[str],
|
||||
anchor: bool = True,
|
||||
prefix: str | None = None,
|
||||
*,
|
||||
is_regex: Literal[True],
|
||||
) -> bool: ...
|
||||
@overload
|
||||
def include_pattern(
|
||||
self,
|
||||
pattern: str | re.Pattern[str],
|
||||
anchor: bool,
|
||||
prefix: str | None,
|
||||
is_regex: Literal[True],
|
||||
) -> bool: ...
|
||||
def include_pattern(
|
||||
self,
|
||||
pattern: str | re.Pattern,
|
||||
anchor: bool = True,
|
||||
prefix: str | None = None,
|
||||
is_regex: bool = False,
|
||||
) -> bool:
|
||||
"""Select strings (presumably filenames) from 'self.files' that
|
||||
match 'pattern', a Unix-style wildcard (glob) pattern. Patterns
|
||||
are not quite the same as implemented by the 'fnmatch' module: '*'
|
||||
and '?' match non-special characters, where "special" is platform-
|
||||
dependent: slash on Unix; colon, slash, and backslash on
|
||||
DOS/Windows; and colon on Mac OS.
|
||||
|
||||
If 'anchor' is true (the default), then the pattern match is more
|
||||
stringent: "*.py" will match "foo.py" but not "foo/bar.py". If
|
||||
'anchor' is false, both of these will match.
|
||||
|
||||
If 'prefix' is supplied, then only filenames starting with 'prefix'
|
||||
(itself a pattern) and ending with 'pattern', with anything in between
|
||||
them, will match. 'anchor' is ignored in this case.
|
||||
|
||||
If 'is_regex' is true, 'anchor' and 'prefix' are ignored, and
|
||||
'pattern' is assumed to be either a string containing a regex or a
|
||||
regex object -- no translation is done, the regex is just compiled
|
||||
and used as-is.
|
||||
|
||||
Selected strings will be added to self.files.
|
||||
|
||||
Return True if files are found, False otherwise.
|
||||
"""
|
||||
# XXX docstring lying about what the special chars are?
|
||||
files_found = False
|
||||
pattern_re = translate_pattern(pattern, anchor, prefix, is_regex)
|
||||
self.debug_print(f"include_pattern: applying regex r'{pattern_re.pattern}'")
|
||||
|
||||
# delayed loading of allfiles list
|
||||
if self.allfiles is None:
|
||||
self.findall()
|
||||
|
||||
for name in self.allfiles:
|
||||
if pattern_re.search(name):
|
||||
self.debug_print(" adding " + name)
|
||||
self.files.append(name)
|
||||
files_found = True
|
||||
return files_found
|
||||
|
||||
@overload
|
||||
def exclude_pattern(
|
||||
self,
|
||||
pattern: str,
|
||||
anchor: bool = True,
|
||||
prefix: str | None = None,
|
||||
is_regex: Literal[False] = False,
|
||||
) -> bool: ...
|
||||
@overload
|
||||
def exclude_pattern(
|
||||
self,
|
||||
pattern: str | re.Pattern[str],
|
||||
anchor: bool = True,
|
||||
prefix: str | None = None,
|
||||
*,
|
||||
is_regex: Literal[True],
|
||||
) -> bool: ...
|
||||
@overload
|
||||
def exclude_pattern(
|
||||
self,
|
||||
pattern: str | re.Pattern[str],
|
||||
anchor: bool,
|
||||
prefix: str | None,
|
||||
is_regex: Literal[True],
|
||||
) -> bool: ...
|
||||
def exclude_pattern(
|
||||
self,
|
||||
pattern: str | re.Pattern,
|
||||
anchor: bool = True,
|
||||
prefix: str | None = None,
|
||||
is_regex: bool = False,
|
||||
) -> bool:
|
||||
"""Remove strings (presumably filenames) from 'files' that match
|
||||
'pattern'. Other parameters are the same as for
|
||||
'include_pattern()', above.
|
||||
The list 'self.files' is modified in place.
|
||||
Return True if files are found, False otherwise.
|
||||
"""
|
||||
files_found = False
|
||||
pattern_re = translate_pattern(pattern, anchor, prefix, is_regex)
|
||||
self.debug_print(f"exclude_pattern: applying regex r'{pattern_re.pattern}'")
|
||||
for i in range(len(self.files) - 1, -1, -1):
|
||||
if pattern_re.search(self.files[i]):
|
||||
self.debug_print(" removing " + self.files[i])
|
||||
del self.files[i]
|
||||
files_found = True
|
||||
return files_found
|
||||
|
||||
|
||||
# Utility functions
|
||||
|
||||
|
||||
def _find_all_simple(path):
|
||||
"""
|
||||
Find all files under 'path'
|
||||
"""
|
||||
all_unique = _UniqueDirs.filter(os.walk(path, followlinks=True))
|
||||
results = (
|
||||
os.path.join(base, file) for base, dirs, files in all_unique for file in files
|
||||
)
|
||||
return filter(os.path.isfile, results)
|
||||
|
||||
|
||||
class _UniqueDirs(set):
|
||||
"""
|
||||
Exclude previously-seen dirs from walk results,
|
||||
avoiding infinite recursion.
|
||||
Ref https://bugs.python.org/issue44497.
|
||||
"""
|
||||
|
||||
def __call__(self, walk_item):
|
||||
"""
|
||||
Given an item from an os.walk result, determine
|
||||
if the item represents a unique dir for this instance
|
||||
and if not, prevent further traversal.
|
||||
"""
|
||||
base, dirs, files = walk_item
|
||||
stat = os.stat(base)
|
||||
candidate = stat.st_dev, stat.st_ino
|
||||
found = candidate in self
|
||||
if found:
|
||||
del dirs[:]
|
||||
self.add(candidate)
|
||||
return not found
|
||||
|
||||
@classmethod
|
||||
def filter(cls, items):
|
||||
return filter(cls(), items)
|
||||
|
||||
|
||||
def findall(dir: str | os.PathLike[str] = os.curdir):
|
||||
"""
|
||||
Find all files under 'dir' and return the list of full filenames.
|
||||
Unless dir is '.', return full filenames with dir prepended.
|
||||
"""
|
||||
files = _find_all_simple(dir)
|
||||
if dir == os.curdir:
|
||||
make_rel = functools.partial(os.path.relpath, start=dir)
|
||||
files = map(make_rel, files)
|
||||
return list(files)
|
||||
|
||||
|
||||
def glob_to_re(pattern):
|
||||
"""Translate a shell-like glob pattern to a regular expression; return
|
||||
a string containing the regex. Differs from 'fnmatch.translate()' in
|
||||
that '*' does not match "special characters" (which are
|
||||
platform-specific).
|
||||
"""
|
||||
pattern_re = fnmatch.translate(pattern)
|
||||
|
||||
# '?' and '*' in the glob pattern become '.' and '.*' in the RE, which
|
||||
# IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix,
|
||||
# and by extension they shouldn't match such "special characters" under
|
||||
# any OS. So change all non-escaped dots in the RE to match any
|
||||
# character except the special characters (currently: just os.sep).
|
||||
sep = os.sep
|
||||
if os.sep == '\\':
|
||||
# we're using a regex to manipulate a regex, so we need
|
||||
# to escape the backslash twice
|
||||
sep = r'\\\\'
|
||||
escaped = rf'\1[^{sep}]'
|
||||
pattern_re = re.sub(r'((?<!\\)(\\\\)*)\.', escaped, pattern_re)
|
||||
return pattern_re
|
||||
|
||||
|
||||
def translate_pattern(pattern, anchor=True, prefix=None, is_regex=False):
|
||||
"""Translate a shell-like wildcard pattern to a compiled regular
|
||||
expression. Return the compiled regex. If 'is_regex' true,
|
||||
then 'pattern' is directly compiled to a regex (if it's a string)
|
||||
or just returned as-is (assumes it's a regex object).
|
||||
"""
|
||||
if is_regex:
|
||||
if isinstance(pattern, str):
|
||||
return re.compile(pattern)
|
||||
else:
|
||||
return pattern
|
||||
|
||||
# ditch start and end characters
|
||||
start, _, end = glob_to_re('_').partition('_')
|
||||
|
||||
if pattern:
|
||||
pattern_re = glob_to_re(pattern)
|
||||
assert pattern_re.startswith(start) and pattern_re.endswith(end)
|
||||
else:
|
||||
pattern_re = ''
|
||||
|
||||
if prefix is not None:
|
||||
prefix_re = glob_to_re(prefix)
|
||||
assert prefix_re.startswith(start) and prefix_re.endswith(end)
|
||||
prefix_re = prefix_re[len(start) : len(prefix_re) - len(end)]
|
||||
sep = os.sep
|
||||
if os.sep == '\\':
|
||||
sep = r'\\'
|
||||
pattern_re = pattern_re[len(start) : len(pattern_re) - len(end)]
|
||||
pattern_re = rf'{start}\A{prefix_re}{sep}.*{pattern_re}{end}'
|
||||
else: # no prefix -- respect anchor flag
|
||||
if anchor:
|
||||
pattern_re = rf'{start}\A{pattern_re[len(start) :]}'
|
||||
|
||||
return re.compile(pattern_re)
|
||||
56
.venv/Lib/site-packages/setuptools/_distutils/log.py
Normal file
56
.venv/Lib/site-packages/setuptools/_distutils/log.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
A simple log mechanism styled after PEP 282.
|
||||
|
||||
Retained for compatibility and should not be used.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from ._log import log as _global_log
|
||||
|
||||
DEBUG = logging.DEBUG
|
||||
INFO = logging.INFO
|
||||
WARN = logging.WARN
|
||||
ERROR = logging.ERROR
|
||||
FATAL = logging.FATAL
|
||||
|
||||
log = _global_log.log
|
||||
debug = _global_log.debug
|
||||
info = _global_log.info
|
||||
warn = _global_log.warning
|
||||
error = _global_log.error
|
||||
fatal = _global_log.fatal
|
||||
|
||||
|
||||
def set_threshold(level):
|
||||
orig = _global_log.level
|
||||
_global_log.setLevel(level)
|
||||
return orig
|
||||
|
||||
|
||||
def set_verbosity(v):
|
||||
if v <= 0:
|
||||
set_threshold(logging.WARN)
|
||||
elif v == 1:
|
||||
set_threshold(logging.INFO)
|
||||
elif v >= 2:
|
||||
set_threshold(logging.DEBUG)
|
||||
|
||||
|
||||
class Log(logging.Logger):
|
||||
"""distutils.log.Log is deprecated, please use an alternative from `logging`."""
|
||||
|
||||
def __init__(self, threshold=WARN):
|
||||
warnings.warn(Log.__doc__) # avoid DeprecationWarning to ensure warn is shown
|
||||
super().__init__(__name__, level=threshold)
|
||||
|
||||
@property
|
||||
def threshold(self):
|
||||
return self.level
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, level):
|
||||
self.setLevel(level)
|
||||
|
||||
warn = logging.Logger.warning
|
||||
134
.venv/Lib/site-packages/setuptools/_distutils/spawn.py
Normal file
134
.venv/Lib/site-packages/setuptools/_distutils/spawn.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""distutils.spawn
|
||||
|
||||
Provides the 'spawn()' function, a front-end to various platform-
|
||||
specific functions for launching another program in a sub-process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import warnings
|
||||
from collections.abc import Mapping, MutableSequence
|
||||
from typing import TYPE_CHECKING, TypeVar, overload
|
||||
|
||||
from ._log import log
|
||||
from .debug import DEBUG
|
||||
from .errors import DistutilsExecError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from subprocess import _ENV
|
||||
|
||||
|
||||
_MappingT = TypeVar("_MappingT", bound=Mapping)
|
||||
|
||||
|
||||
def _debug(cmd):
|
||||
"""
|
||||
Render a subprocess command differently depending on DEBUG.
|
||||
"""
|
||||
return cmd if DEBUG else cmd[0]
|
||||
|
||||
|
||||
def _inject_macos_ver(env: _MappingT | None) -> _MappingT | dict[str, str | int] | None:
|
||||
if platform.system() != 'Darwin':
|
||||
return env
|
||||
|
||||
from .util import MACOSX_VERSION_VAR, get_macosx_target_ver
|
||||
|
||||
target_ver = get_macosx_target_ver()
|
||||
update = {MACOSX_VERSION_VAR: target_ver} if target_ver else {}
|
||||
return {**_resolve(env), **update}
|
||||
|
||||
|
||||
@overload
|
||||
def _resolve(env: None) -> os._Environ[str]: ...
|
||||
@overload
|
||||
def _resolve(env: _MappingT) -> _MappingT: ...
|
||||
def _resolve(env: _MappingT | None) -> _MappingT | os._Environ[str]:
|
||||
return os.environ if env is None else env
|
||||
|
||||
|
||||
def spawn(
|
||||
cmd: MutableSequence[bytes | str | os.PathLike[str]],
|
||||
search_path: bool = True,
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False,
|
||||
env: _ENV | None = None,
|
||||
) -> None:
|
||||
"""Run another program, specified as a command list 'cmd', in a new process.
|
||||
|
||||
'cmd' is just the argument list for the new process, ie.
|
||||
cmd[0] is the program to run and cmd[1:] are the rest of its arguments.
|
||||
There is no way to run a program with a name different from that of its
|
||||
executable.
|
||||
|
||||
If 'search_path' is true (the default), the system's executable
|
||||
search path will be used to find the program; otherwise, cmd[0]
|
||||
must be the exact path to the executable. If 'dry_run' is true,
|
||||
the command will not actually be run.
|
||||
|
||||
Raise DistutilsExecError if running the program fails in any way; just
|
||||
return on success.
|
||||
"""
|
||||
log.info(subprocess.list2cmdline(cmd))
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
if search_path:
|
||||
executable = shutil.which(cmd[0])
|
||||
if executable is not None:
|
||||
cmd[0] = executable
|
||||
|
||||
try:
|
||||
subprocess.check_call(cmd, env=_inject_macos_ver(env))
|
||||
except OSError as exc:
|
||||
raise DistutilsExecError(
|
||||
f"command {_debug(cmd)!r} failed: {exc.args[-1]}"
|
||||
) from exc
|
||||
except subprocess.CalledProcessError as err:
|
||||
raise DistutilsExecError(
|
||||
f"command {_debug(cmd)!r} failed with exit code {err.returncode}"
|
||||
) from err
|
||||
|
||||
|
||||
def find_executable(executable: str, path: str | None = None) -> str | None:
|
||||
"""Tries to find 'executable' in the directories listed in 'path'.
|
||||
|
||||
A string listing directories separated by 'os.pathsep'; defaults to
|
||||
os.environ['PATH']. Returns the complete filename or None if not found.
|
||||
"""
|
||||
warnings.warn(
|
||||
'Use shutil.which instead of find_executable', DeprecationWarning, stacklevel=2
|
||||
)
|
||||
_, ext = os.path.splitext(executable)
|
||||
if (sys.platform == 'win32') and (ext != '.exe'):
|
||||
executable = executable + '.exe'
|
||||
|
||||
if os.path.isfile(executable):
|
||||
return executable
|
||||
|
||||
if path is None:
|
||||
path = os.environ.get('PATH', None)
|
||||
# bpo-35755: Don't fall through if PATH is the empty string
|
||||
if path is None:
|
||||
try:
|
||||
path = os.confstr("CS_PATH")
|
||||
except (AttributeError, ValueError):
|
||||
# os.confstr() or CS_PATH is not available
|
||||
path = os.defpath
|
||||
|
||||
# PATH='' doesn't match, whereas PATH=':' looks in the current directory
|
||||
if not path:
|
||||
return None
|
||||
|
||||
paths = path.split(os.pathsep)
|
||||
for p in paths:
|
||||
f = os.path.join(p, executable)
|
||||
if os.path.isfile(f):
|
||||
# the file exists, we have a shot at spawn working
|
||||
return f
|
||||
return None
|
||||
598
.venv/Lib/site-packages/setuptools/_distutils/sysconfig.py
Normal file
598
.venv/Lib/site-packages/setuptools/_distutils/sysconfig.py
Normal file
@@ -0,0 +1,598 @@
|
||||
"""Provide access to Python's configuration information. The specific
|
||||
configuration variables available depend heavily on the platform and
|
||||
configuration. The values may be retrieved using
|
||||
get_config_var(name), and the list of variables is available via
|
||||
get_config_vars().keys(). Additional convenience functions are also
|
||||
available.
|
||||
|
||||
Written by: Fred L. Drake, Jr.
|
||||
Email: <fdrake@acm.org>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import sysconfig
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
|
||||
from jaraco.functools import pass_none
|
||||
|
||||
from .ccompiler import CCompiler
|
||||
from .compat import py39
|
||||
from .errors import DistutilsPlatformError
|
||||
from .util import is_mingw
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import deprecated
|
||||
else:
|
||||
|
||||
def deprecated(message):
|
||||
return lambda fn: fn
|
||||
|
||||
|
||||
IS_PYPY = '__pypy__' in sys.builtin_module_names
|
||||
|
||||
# These are needed in a couple of spots, so just compute them once.
|
||||
PREFIX = os.path.normpath(sys.prefix)
|
||||
EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
|
||||
BASE_PREFIX = os.path.normpath(sys.base_prefix)
|
||||
BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix)
|
||||
|
||||
# Path to the base directory of the project. On Windows the binary may
|
||||
# live in project/PCbuild/win32 or project/PCbuild/amd64.
|
||||
# set for cross builds
|
||||
if "_PYTHON_PROJECT_BASE" in os.environ:
|
||||
project_base = os.path.abspath(os.environ["_PYTHON_PROJECT_BASE"])
|
||||
else:
|
||||
if sys.executable:
|
||||
project_base = os.path.dirname(os.path.abspath(sys.executable))
|
||||
else:
|
||||
# sys.executable can be empty if argv[0] has been changed and Python is
|
||||
# unable to retrieve the real program name
|
||||
project_base = os.getcwd()
|
||||
|
||||
|
||||
def _is_python_source_dir(d):
|
||||
"""
|
||||
Return True if the target directory appears to point to an
|
||||
un-installed Python.
|
||||
"""
|
||||
modules = pathlib.Path(d).joinpath('Modules')
|
||||
return any(modules.joinpath(fn).is_file() for fn in ('Setup', 'Setup.local'))
|
||||
|
||||
|
||||
_sys_home = getattr(sys, '_home', None)
|
||||
|
||||
|
||||
def _is_parent(dir_a, dir_b):
|
||||
"""
|
||||
Return True if a is a parent of b.
|
||||
"""
|
||||
return os.path.normcase(dir_a).startswith(os.path.normcase(dir_b))
|
||||
|
||||
|
||||
if os.name == 'nt':
|
||||
|
||||
@pass_none
|
||||
def _fix_pcbuild(d):
|
||||
# In a venv, sys._home will be inside BASE_PREFIX rather than PREFIX.
|
||||
prefixes = PREFIX, BASE_PREFIX
|
||||
matched = (
|
||||
prefix
|
||||
for prefix in prefixes
|
||||
if _is_parent(d, os.path.join(prefix, "PCbuild"))
|
||||
)
|
||||
return next(matched, d)
|
||||
|
||||
project_base = _fix_pcbuild(project_base)
|
||||
_sys_home = _fix_pcbuild(_sys_home)
|
||||
|
||||
|
||||
def _python_build():
|
||||
if _sys_home:
|
||||
return _is_python_source_dir(_sys_home)
|
||||
return _is_python_source_dir(project_base)
|
||||
|
||||
|
||||
python_build = _python_build()
|
||||
|
||||
|
||||
# Calculate the build qualifier flags if they are defined. Adding the flags
|
||||
# to the include and lib directories only makes sense for an installation, not
|
||||
# an in-source build.
|
||||
build_flags = ''
|
||||
try:
|
||||
if not python_build:
|
||||
build_flags = sys.abiflags
|
||||
except AttributeError:
|
||||
# It's not a configure-based build, so the sys module doesn't have
|
||||
# this attribute, which is fine.
|
||||
pass
|
||||
|
||||
|
||||
def get_python_version():
|
||||
"""Return a string containing the major and minor Python version,
|
||||
leaving off the patchlevel. Sample return values could be '1.5'
|
||||
or '2.2'.
|
||||
"""
|
||||
return f'{sys.version_info.major}.{sys.version_info.minor}'
|
||||
|
||||
|
||||
def get_python_inc(plat_specific: bool = False, prefix: str | None = None) -> str:
|
||||
"""Return the directory containing installed Python header files.
|
||||
|
||||
If 'plat_specific' is false (the default), this is the path to the
|
||||
non-platform-specific header files, i.e. Python.h and so on;
|
||||
otherwise, this is the path to platform-specific header files
|
||||
(namely pyconfig.h).
|
||||
|
||||
If 'prefix' is supplied, use it instead of sys.base_prefix or
|
||||
sys.base_exec_prefix -- i.e., ignore 'plat_specific'.
|
||||
"""
|
||||
default_prefix = BASE_EXEC_PREFIX if plat_specific else BASE_PREFIX
|
||||
resolved_prefix = prefix if prefix is not None else default_prefix
|
||||
# MinGW imitates posix like layout, but os.name != posix
|
||||
os_name = "posix" if is_mingw() else os.name
|
||||
try:
|
||||
getter = globals()[f'_get_python_inc_{os_name}']
|
||||
except KeyError:
|
||||
raise DistutilsPlatformError(
|
||||
"I don't know where Python installs its C header files "
|
||||
f"on platform '{os.name}'"
|
||||
)
|
||||
return getter(resolved_prefix, prefix, plat_specific)
|
||||
|
||||
|
||||
@pass_none
|
||||
def _extant(path):
|
||||
"""
|
||||
Replace path with None if it doesn't exist.
|
||||
"""
|
||||
return path if os.path.exists(path) else None
|
||||
|
||||
|
||||
def _get_python_inc_posix(prefix, spec_prefix, plat_specific):
|
||||
return (
|
||||
_get_python_inc_posix_python(plat_specific)
|
||||
or _extant(_get_python_inc_from_config(plat_specific, spec_prefix))
|
||||
or _get_python_inc_posix_prefix(prefix)
|
||||
)
|
||||
|
||||
|
||||
def _get_python_inc_posix_python(plat_specific):
|
||||
"""
|
||||
Assume the executable is in the build directory. The
|
||||
pyconfig.h file should be in the same directory. Since
|
||||
the build directory may not be the source directory,
|
||||
use "srcdir" from the makefile to find the "Include"
|
||||
directory.
|
||||
"""
|
||||
if not python_build:
|
||||
return
|
||||
if plat_specific:
|
||||
return _sys_home or project_base
|
||||
incdir = os.path.join(get_config_var('srcdir'), 'Include')
|
||||
return os.path.normpath(incdir)
|
||||
|
||||
|
||||
def _get_python_inc_from_config(plat_specific, spec_prefix):
|
||||
"""
|
||||
If no prefix was explicitly specified, provide the include
|
||||
directory from the config vars. Useful when
|
||||
cross-compiling, since the config vars may come from
|
||||
the host
|
||||
platform Python installation, while the current Python
|
||||
executable is from the build platform installation.
|
||||
|
||||
>>> monkeypatch = getfixture('monkeypatch')
|
||||
>>> gpifc = _get_python_inc_from_config
|
||||
>>> monkeypatch.setitem(gpifc.__globals__, 'get_config_var', str.lower)
|
||||
>>> gpifc(False, '/usr/bin/')
|
||||
>>> gpifc(False, '')
|
||||
>>> gpifc(False, None)
|
||||
'includepy'
|
||||
>>> gpifc(True, None)
|
||||
'confincludepy'
|
||||
"""
|
||||
if spec_prefix is None:
|
||||
return get_config_var('CONF' * plat_specific + 'INCLUDEPY')
|
||||
|
||||
|
||||
def _get_python_inc_posix_prefix(prefix):
|
||||
implementation = 'pypy' if IS_PYPY else 'python'
|
||||
python_dir = implementation + get_python_version() + build_flags
|
||||
return os.path.join(prefix, "include", python_dir)
|
||||
|
||||
|
||||
def _get_python_inc_nt(prefix, spec_prefix, plat_specific):
|
||||
if python_build:
|
||||
# Include both include dirs to ensure we can find pyconfig.h
|
||||
return (
|
||||
os.path.join(prefix, "include")
|
||||
+ os.path.pathsep
|
||||
+ os.path.dirname(sysconfig.get_config_h_filename())
|
||||
)
|
||||
return os.path.join(prefix, "include")
|
||||
|
||||
|
||||
# allow this behavior to be monkey-patched. Ref pypa/distutils#2.
|
||||
def _posix_lib(standard_lib, libpython, early_prefix, prefix):
|
||||
if standard_lib:
|
||||
return libpython
|
||||
else:
|
||||
return os.path.join(libpython, "site-packages")
|
||||
|
||||
|
||||
def get_python_lib(
|
||||
plat_specific: bool = False, standard_lib: bool = False, prefix: str | None = None
|
||||
) -> str:
|
||||
"""Return the directory containing the Python library (standard or
|
||||
site additions).
|
||||
|
||||
If 'plat_specific' is true, return the directory containing
|
||||
platform-specific modules, i.e. any module from a non-pure-Python
|
||||
module distribution; otherwise, return the platform-shared library
|
||||
directory. If 'standard_lib' is true, return the directory
|
||||
containing standard Python library modules; otherwise, return the
|
||||
directory for site-specific modules.
|
||||
|
||||
If 'prefix' is supplied, use it instead of sys.base_prefix or
|
||||
sys.base_exec_prefix -- i.e., ignore 'plat_specific'.
|
||||
"""
|
||||
|
||||
early_prefix = prefix
|
||||
|
||||
if prefix is None:
|
||||
if standard_lib:
|
||||
prefix = plat_specific and BASE_EXEC_PREFIX or BASE_PREFIX
|
||||
else:
|
||||
prefix = plat_specific and EXEC_PREFIX or PREFIX
|
||||
|
||||
if os.name == "posix" or is_mingw():
|
||||
if plat_specific or standard_lib:
|
||||
# Platform-specific modules (any module from a non-pure-Python
|
||||
# module distribution) or standard Python library modules.
|
||||
libdir = getattr(sys, "platlibdir", "lib")
|
||||
else:
|
||||
# Pure Python
|
||||
libdir = "lib"
|
||||
implementation = 'pypy' if IS_PYPY else 'python'
|
||||
libpython = os.path.join(prefix, libdir, implementation + get_python_version())
|
||||
return _posix_lib(standard_lib, libpython, early_prefix, prefix)
|
||||
elif os.name == "nt":
|
||||
if standard_lib:
|
||||
return os.path.join(prefix, "Lib")
|
||||
else:
|
||||
return os.path.join(prefix, "Lib", "site-packages")
|
||||
else:
|
||||
raise DistutilsPlatformError(
|
||||
f"I don't know where Python installs its library on platform '{os.name}'"
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _customize_macos():
|
||||
"""
|
||||
Perform first-time customization of compiler-related
|
||||
config vars on macOS. Use after a compiler is known
|
||||
to be needed. This customization exists primarily to support Pythons
|
||||
from binary installers. The kind and paths to build tools on
|
||||
the user system may vary significantly from the system
|
||||
that Python itself was built on. Also the user OS
|
||||
version and build tools may not support the same set
|
||||
of CPU architectures for universal builds.
|
||||
"""
|
||||
|
||||
sys.platform == "darwin" and __import__('_osx_support').customize_compiler(
|
||||
get_config_vars()
|
||||
)
|
||||
|
||||
|
||||
def customize_compiler(compiler: CCompiler) -> None:
|
||||
"""Do any platform-specific customization of a CCompiler instance.
|
||||
|
||||
Mainly needed on Unix, so we can plug in the information that
|
||||
varies across Unices and is stored in Python's Makefile.
|
||||
"""
|
||||
if compiler.compiler_type in ["unix", "cygwin"] or (
|
||||
compiler.compiler_type == "mingw32" and is_mingw()
|
||||
):
|
||||
_customize_macos()
|
||||
|
||||
(
|
||||
cc,
|
||||
cxx,
|
||||
cflags,
|
||||
ccshared,
|
||||
ldshared,
|
||||
ldcxxshared,
|
||||
shlib_suffix,
|
||||
ar,
|
||||
ar_flags,
|
||||
) = get_config_vars(
|
||||
'CC',
|
||||
'CXX',
|
||||
'CFLAGS',
|
||||
'CCSHARED',
|
||||
'LDSHARED',
|
||||
'LDCXXSHARED',
|
||||
'SHLIB_SUFFIX',
|
||||
'AR',
|
||||
'ARFLAGS',
|
||||
)
|
||||
|
||||
cxxflags = cflags
|
||||
|
||||
if 'CC' in os.environ:
|
||||
newcc = os.environ['CC']
|
||||
if 'LDSHARED' not in os.environ and ldshared.startswith(cc):
|
||||
# If CC is overridden, use that as the default
|
||||
# command for LDSHARED as well
|
||||
ldshared = newcc + ldshared[len(cc) :]
|
||||
cc = newcc
|
||||
cxx = os.environ.get('CXX', cxx)
|
||||
ldshared = os.environ.get('LDSHARED', ldshared)
|
||||
ldcxxshared = os.environ.get('LDCXXSHARED', ldcxxshared)
|
||||
cpp = os.environ.get(
|
||||
'CPP',
|
||||
cc + " -E", # not always
|
||||
)
|
||||
|
||||
ldshared = _add_flags(ldshared, 'LD')
|
||||
ldcxxshared = _add_flags(ldcxxshared, 'LD')
|
||||
cflags = os.environ.get('CFLAGS', cflags)
|
||||
ldshared = _add_flags(ldshared, 'C')
|
||||
cxxflags = os.environ.get('CXXFLAGS', cxxflags)
|
||||
ldcxxshared = _add_flags(ldcxxshared, 'CXX')
|
||||
cpp = _add_flags(cpp, 'CPP')
|
||||
cflags = _add_flags(cflags, 'CPP')
|
||||
cxxflags = _add_flags(cxxflags, 'CPP')
|
||||
ldshared = _add_flags(ldshared, 'CPP')
|
||||
ldcxxshared = _add_flags(ldcxxshared, 'CPP')
|
||||
|
||||
ar = os.environ.get('AR', ar)
|
||||
|
||||
archiver = ar + ' ' + os.environ.get('ARFLAGS', ar_flags)
|
||||
cc_cmd = cc + ' ' + cflags
|
||||
cxx_cmd = cxx + ' ' + cxxflags
|
||||
|
||||
compiler.set_executables(
|
||||
preprocessor=cpp,
|
||||
compiler=cc_cmd,
|
||||
compiler_so=cc_cmd + ' ' + ccshared,
|
||||
compiler_cxx=cxx_cmd,
|
||||
compiler_so_cxx=cxx_cmd + ' ' + ccshared,
|
||||
linker_so=ldshared,
|
||||
linker_so_cxx=ldcxxshared,
|
||||
linker_exe=cc,
|
||||
linker_exe_cxx=cxx,
|
||||
archiver=archiver,
|
||||
)
|
||||
|
||||
if 'RANLIB' in os.environ and compiler.executables.get('ranlib', None):
|
||||
compiler.set_executables(ranlib=os.environ['RANLIB'])
|
||||
|
||||
compiler.shared_lib_extension = shlib_suffix
|
||||
|
||||
|
||||
def get_config_h_filename() -> str:
|
||||
"""Return full pathname of installed pyconfig.h file."""
|
||||
return sysconfig.get_config_h_filename()
|
||||
|
||||
|
||||
def get_makefile_filename() -> str:
|
||||
"""Return full pathname of installed Makefile from the Python build."""
|
||||
return sysconfig.get_makefile_filename()
|
||||
|
||||
|
||||
def parse_config_h(fp, g=None):
|
||||
"""Parse a config.h-style file.
|
||||
|
||||
A dictionary containing name/value pairs is returned. If an
|
||||
optional dictionary is passed in as the second argument, it is
|
||||
used instead of a new dictionary.
|
||||
"""
|
||||
return sysconfig.parse_config_h(fp, vars=g)
|
||||
|
||||
|
||||
# Regexes needed for parsing Makefile (and similar syntaxes,
|
||||
# like old-style Setup files).
|
||||
_variable_rx = re.compile(r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)")
|
||||
_findvar1_rx = re.compile(r"\$\(([A-Za-z][A-Za-z0-9_]*)\)")
|
||||
_findvar2_rx = re.compile(r"\${([A-Za-z][A-Za-z0-9_]*)}")
|
||||
|
||||
|
||||
def parse_makefile(fn, g=None): # noqa: C901
|
||||
"""Parse a Makefile-style file.
|
||||
|
||||
A dictionary containing name/value pairs is returned. If an
|
||||
optional dictionary is passed in as the second argument, it is
|
||||
used instead of a new dictionary.
|
||||
"""
|
||||
from distutils.text_file import TextFile
|
||||
|
||||
fp = TextFile(
|
||||
fn,
|
||||
strip_comments=True,
|
||||
skip_blanks=True,
|
||||
join_lines=True,
|
||||
errors="surrogateescape",
|
||||
)
|
||||
|
||||
if g is None:
|
||||
g = {}
|
||||
done = {}
|
||||
notdone = {}
|
||||
|
||||
while True:
|
||||
line = fp.readline()
|
||||
if line is None: # eof
|
||||
break
|
||||
m = _variable_rx.match(line)
|
||||
if m:
|
||||
n, v = m.group(1, 2)
|
||||
v = v.strip()
|
||||
# `$$' is a literal `$' in make
|
||||
tmpv = v.replace('$$', '')
|
||||
|
||||
if "$" in tmpv:
|
||||
notdone[n] = v
|
||||
else:
|
||||
try:
|
||||
v = int(v)
|
||||
except ValueError:
|
||||
# insert literal `$'
|
||||
done[n] = v.replace('$$', '$')
|
||||
else:
|
||||
done[n] = v
|
||||
|
||||
# Variables with a 'PY_' prefix in the makefile. These need to
|
||||
# be made available without that prefix through sysconfig.
|
||||
# Special care is needed to ensure that variable expansion works, even
|
||||
# if the expansion uses the name without a prefix.
|
||||
renamed_variables = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS')
|
||||
|
||||
# do variable interpolation here
|
||||
while notdone:
|
||||
for name in list(notdone):
|
||||
value = notdone[name]
|
||||
m = _findvar1_rx.search(value) or _findvar2_rx.search(value)
|
||||
if m:
|
||||
n = m.group(1)
|
||||
found = True
|
||||
if n in done:
|
||||
item = str(done[n])
|
||||
elif n in notdone:
|
||||
# get it on a subsequent round
|
||||
found = False
|
||||
elif n in os.environ:
|
||||
# do it like make: fall back to environment
|
||||
item = os.environ[n]
|
||||
|
||||
elif n in renamed_variables:
|
||||
if name.startswith('PY_') and name[3:] in renamed_variables:
|
||||
item = ""
|
||||
|
||||
elif 'PY_' + n in notdone:
|
||||
found = False
|
||||
|
||||
else:
|
||||
item = str(done['PY_' + n])
|
||||
else:
|
||||
done[n] = item = ""
|
||||
if found:
|
||||
after = value[m.end() :]
|
||||
value = value[: m.start()] + item + after
|
||||
if "$" in after:
|
||||
notdone[name] = value
|
||||
else:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
done[name] = value.strip()
|
||||
else:
|
||||
done[name] = value
|
||||
del notdone[name]
|
||||
|
||||
if name.startswith('PY_') and name[3:] in renamed_variables:
|
||||
name = name[3:]
|
||||
if name not in done:
|
||||
done[name] = value
|
||||
else:
|
||||
# bogus variable reference; just drop it since we can't deal
|
||||
del notdone[name]
|
||||
|
||||
fp.close()
|
||||
|
||||
# strip spurious spaces
|
||||
for k, v in done.items():
|
||||
if isinstance(v, str):
|
||||
done[k] = v.strip()
|
||||
|
||||
# save the results in the global dictionary
|
||||
g.update(done)
|
||||
return g
|
||||
|
||||
|
||||
def expand_makefile_vars(s, vars):
|
||||
"""Expand Makefile-style variables -- "${foo}" or "$(foo)" -- in
|
||||
'string' according to 'vars' (a dictionary mapping variable names to
|
||||
values). Variables not present in 'vars' are silently expanded to the
|
||||
empty string. The variable values in 'vars' should not contain further
|
||||
variable expansions; if 'vars' is the output of 'parse_makefile()',
|
||||
you're fine. Returns a variable-expanded version of 's'.
|
||||
"""
|
||||
|
||||
# This algorithm does multiple expansion, so if vars['foo'] contains
|
||||
# "${bar}", it will expand ${foo} to ${bar}, and then expand
|
||||
# ${bar}... and so forth. This is fine as long as 'vars' comes from
|
||||
# 'parse_makefile()', which takes care of such expansions eagerly,
|
||||
# according to make's variable expansion semantics.
|
||||
|
||||
while True:
|
||||
m = _findvar1_rx.search(s) or _findvar2_rx.search(s)
|
||||
if m:
|
||||
(beg, end) = m.span()
|
||||
s = s[0:beg] + vars.get(m.group(1)) + s[end:]
|
||||
else:
|
||||
break
|
||||
return s
|
||||
|
||||
|
||||
_config_vars = None
|
||||
|
||||
|
||||
@overload
|
||||
def get_config_vars() -> dict[str, str | int]: ...
|
||||
@overload
|
||||
def get_config_vars(arg: str, /, *args: str) -> list[str | int]: ...
|
||||
def get_config_vars(*args: str) -> list[str | int] | dict[str, str | int]:
|
||||
"""With no arguments, return a dictionary of all configuration
|
||||
variables relevant for the current platform. Generally this includes
|
||||
everything needed to build extensions and install both pure modules and
|
||||
extensions. On Unix, this means every variable defined in Python's
|
||||
installed Makefile; on Windows it's a much smaller set.
|
||||
|
||||
With arguments, return a list of values that result from looking up
|
||||
each argument in the configuration variable dictionary.
|
||||
"""
|
||||
global _config_vars
|
||||
if _config_vars is None:
|
||||
_config_vars = sysconfig.get_config_vars().copy()
|
||||
py39.add_ext_suffix(_config_vars)
|
||||
|
||||
return [_config_vars.get(name) for name in args] if args else _config_vars
|
||||
|
||||
|
||||
@overload
|
||||
@deprecated(
|
||||
"SO is deprecated, use EXT_SUFFIX. Support will be removed when this module is synchronized with stdlib Python 3.11"
|
||||
)
|
||||
def get_config_var(name: Literal["SO"]) -> int | str | None: ...
|
||||
@overload
|
||||
def get_config_var(name: str) -> int | str | None: ...
|
||||
def get_config_var(name: str) -> int | str | None:
|
||||
"""Return the value of a single variable using the dictionary
|
||||
returned by 'get_config_vars()'. Equivalent to
|
||||
get_config_vars().get(name)
|
||||
"""
|
||||
if name == 'SO':
|
||||
import warnings
|
||||
|
||||
warnings.warn('SO is deprecated, use EXT_SUFFIX', DeprecationWarning, 2)
|
||||
return get_config_vars().get(name)
|
||||
|
||||
|
||||
@pass_none
|
||||
def _add_flags(value: str, type: str) -> str:
|
||||
"""
|
||||
Add any flags from the environment for the given type.
|
||||
|
||||
type is the prefix to FLAGS in the environment key (e.g. "C" for "CFLAGS").
|
||||
"""
|
||||
flags = os.environ.get(f'{type}FLAGS')
|
||||
return f'{value} {flags}' if flags else value
|
||||
286
.venv/Lib/site-packages/setuptools/_distutils/text_file.py
Normal file
286
.venv/Lib/site-packages/setuptools/_distutils/text_file.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""text_file
|
||||
|
||||
provides the TextFile class, which gives an interface to text files
|
||||
that (optionally) takes care of stripping comments, ignoring blank
|
||||
lines, and joining lines with backslashes."""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class TextFile:
|
||||
"""Provides a file-like object that takes care of all the things you
|
||||
commonly want to do when processing a text file that has some
|
||||
line-by-line syntax: strip comments (as long as "#" is your
|
||||
comment character), skip blank lines, join adjacent lines by
|
||||
escaping the newline (ie. backslash at end of line), strip
|
||||
leading and/or trailing whitespace. All of these are optional
|
||||
and independently controllable.
|
||||
|
||||
Provides a 'warn()' method so you can generate warning messages that
|
||||
report physical line number, even if the logical line in question
|
||||
spans multiple physical lines. Also provides 'unreadline()' for
|
||||
implementing line-at-a-time lookahead.
|
||||
|
||||
Constructor is called as:
|
||||
|
||||
TextFile (filename=None, file=None, **options)
|
||||
|
||||
It bombs (RuntimeError) if both 'filename' and 'file' are None;
|
||||
'filename' should be a string, and 'file' a file object (or
|
||||
something that provides 'readline()' and 'close()' methods). It is
|
||||
recommended that you supply at least 'filename', so that TextFile
|
||||
can include it in warning messages. If 'file' is not supplied,
|
||||
TextFile creates its own using 'io.open()'.
|
||||
|
||||
The options are all boolean, and affect the value returned by
|
||||
'readline()':
|
||||
strip_comments [default: true]
|
||||
strip from "#" to end-of-line, as well as any whitespace
|
||||
leading up to the "#" -- unless it is escaped by a backslash
|
||||
lstrip_ws [default: false]
|
||||
strip leading whitespace from each line before returning it
|
||||
rstrip_ws [default: true]
|
||||
strip trailing whitespace (including line terminator!) from
|
||||
each line before returning it
|
||||
skip_blanks [default: true}
|
||||
skip lines that are empty *after* stripping comments and
|
||||
whitespace. (If both lstrip_ws and rstrip_ws are false,
|
||||
then some lines may consist of solely whitespace: these will
|
||||
*not* be skipped, even if 'skip_blanks' is true.)
|
||||
join_lines [default: false]
|
||||
if a backslash is the last non-newline character on a line
|
||||
after stripping comments and whitespace, join the following line
|
||||
to it to form one "logical line"; if N consecutive lines end
|
||||
with a backslash, then N+1 physical lines will be joined to
|
||||
form one logical line.
|
||||
collapse_join [default: false]
|
||||
strip leading whitespace from lines that are joined to their
|
||||
predecessor; only matters if (join_lines and not lstrip_ws)
|
||||
errors [default: 'strict']
|
||||
error handler used to decode the file content
|
||||
|
||||
Note that since 'rstrip_ws' can strip the trailing newline, the
|
||||
semantics of 'readline()' must differ from those of the builtin file
|
||||
object's 'readline()' method! In particular, 'readline()' returns
|
||||
None for end-of-file: an empty string might just be a blank line (or
|
||||
an all-whitespace line), if 'rstrip_ws' is true but 'skip_blanks' is
|
||||
not."""
|
||||
|
||||
default_options = {
|
||||
'strip_comments': 1,
|
||||
'skip_blanks': 1,
|
||||
'lstrip_ws': 0,
|
||||
'rstrip_ws': 1,
|
||||
'join_lines': 0,
|
||||
'collapse_join': 0,
|
||||
'errors': 'strict',
|
||||
}
|
||||
|
||||
def __init__(self, filename=None, file=None, **options):
|
||||
"""Construct a new TextFile object. At least one of 'filename'
|
||||
(a string) and 'file' (a file-like object) must be supplied.
|
||||
They keyword argument options are described above and affect
|
||||
the values returned by 'readline()'."""
|
||||
if filename is None and file is None:
|
||||
raise RuntimeError(
|
||||
"you must supply either or both of 'filename' and 'file'"
|
||||
)
|
||||
|
||||
# set values for all options -- either from client option hash
|
||||
# or fallback to default_options
|
||||
for opt in self.default_options.keys():
|
||||
if opt in options:
|
||||
setattr(self, opt, options[opt])
|
||||
else:
|
||||
setattr(self, opt, self.default_options[opt])
|
||||
|
||||
# sanity check client option hash
|
||||
for opt in options.keys():
|
||||
if opt not in self.default_options:
|
||||
raise KeyError(f"invalid TextFile option '{opt}'")
|
||||
|
||||
if file is None:
|
||||
self.open(filename)
|
||||
else:
|
||||
self.filename = filename
|
||||
self.file = file
|
||||
self.current_line = 0 # assuming that file is at BOF!
|
||||
|
||||
# 'linebuf' is a stack of lines that will be emptied before we
|
||||
# actually read from the file; it's only populated by an
|
||||
# 'unreadline()' operation
|
||||
self.linebuf = []
|
||||
|
||||
def open(self, filename):
|
||||
"""Open a new file named 'filename'. This overrides both the
|
||||
'filename' and 'file' arguments to the constructor."""
|
||||
self.filename = filename
|
||||
self.file = open(self.filename, errors=self.errors, encoding='utf-8')
|
||||
self.current_line = 0
|
||||
|
||||
def close(self):
|
||||
"""Close the current file and forget everything we know about it
|
||||
(filename, current line number)."""
|
||||
file = self.file
|
||||
self.file = None
|
||||
self.filename = None
|
||||
self.current_line = None
|
||||
file.close()
|
||||
|
||||
def gen_error(self, msg, line=None):
|
||||
outmsg = []
|
||||
if line is None:
|
||||
line = self.current_line
|
||||
outmsg.append(self.filename + ", ")
|
||||
if isinstance(line, (list, tuple)):
|
||||
outmsg.append("lines {}-{}: ".format(*line))
|
||||
else:
|
||||
outmsg.append(f"line {int(line)}: ")
|
||||
outmsg.append(str(msg))
|
||||
return "".join(outmsg)
|
||||
|
||||
def error(self, msg, line=None):
|
||||
raise ValueError("error: " + self.gen_error(msg, line))
|
||||
|
||||
def warn(self, msg, line=None):
|
||||
"""Print (to stderr) a warning message tied to the current logical
|
||||
line in the current file. If the current logical line in the
|
||||
file spans multiple physical lines, the warning refers to the
|
||||
whole range, eg. "lines 3-5". If 'line' supplied, it overrides
|
||||
the current line number; it may be a list or tuple to indicate a
|
||||
range of physical lines, or an integer for a single physical
|
||||
line."""
|
||||
sys.stderr.write("warning: " + self.gen_error(msg, line) + "\n")
|
||||
|
||||
def readline(self): # noqa: C901
|
||||
"""Read and return a single logical line from the current file (or
|
||||
from an internal buffer if lines have previously been "unread"
|
||||
with 'unreadline()'). If the 'join_lines' option is true, this
|
||||
may involve reading multiple physical lines concatenated into a
|
||||
single string. Updates the current line number, so calling
|
||||
'warn()' after 'readline()' emits a warning about the physical
|
||||
line(s) just read. Returns None on end-of-file, since the empty
|
||||
string can occur if 'rstrip_ws' is true but 'strip_blanks' is
|
||||
not."""
|
||||
# If any "unread" lines waiting in 'linebuf', return the top
|
||||
# one. (We don't actually buffer read-ahead data -- lines only
|
||||
# get put in 'linebuf' if the client explicitly does an
|
||||
# 'unreadline()'.
|
||||
if self.linebuf:
|
||||
line = self.linebuf[-1]
|
||||
del self.linebuf[-1]
|
||||
return line
|
||||
|
||||
buildup_line = ''
|
||||
|
||||
while True:
|
||||
# read the line, make it None if EOF
|
||||
line = self.file.readline()
|
||||
if line == '':
|
||||
line = None
|
||||
|
||||
if self.strip_comments and line:
|
||||
# Look for the first "#" in the line. If none, never
|
||||
# mind. If we find one and it's the first character, or
|
||||
# is not preceded by "\", then it starts a comment --
|
||||
# strip the comment, strip whitespace before it, and
|
||||
# carry on. Otherwise, it's just an escaped "#", so
|
||||
# unescape it (and any other escaped "#"'s that might be
|
||||
# lurking in there) and otherwise leave the line alone.
|
||||
|
||||
pos = line.find("#")
|
||||
if pos == -1: # no "#" -- no comments
|
||||
pass
|
||||
|
||||
# It's definitely a comment -- either "#" is the first
|
||||
# character, or it's elsewhere and unescaped.
|
||||
elif pos == 0 or line[pos - 1] != "\\":
|
||||
# Have to preserve the trailing newline, because it's
|
||||
# the job of a later step (rstrip_ws) to remove it --
|
||||
# and if rstrip_ws is false, we'd better preserve it!
|
||||
# (NB. this means that if the final line is all comment
|
||||
# and has no trailing newline, we will think that it's
|
||||
# EOF; I think that's OK.)
|
||||
eol = (line[-1] == '\n') and '\n' or ''
|
||||
line = line[0:pos] + eol
|
||||
|
||||
# If all that's left is whitespace, then skip line
|
||||
# *now*, before we try to join it to 'buildup_line' --
|
||||
# that way constructs like
|
||||
# hello \\
|
||||
# # comment that should be ignored
|
||||
# there
|
||||
# result in "hello there".
|
||||
if line.strip() == "":
|
||||
continue
|
||||
else: # it's an escaped "#"
|
||||
line = line.replace("\\#", "#")
|
||||
|
||||
# did previous line end with a backslash? then accumulate
|
||||
if self.join_lines and buildup_line:
|
||||
# oops: end of file
|
||||
if line is None:
|
||||
self.warn("continuation line immediately precedes end-of-file")
|
||||
return buildup_line
|
||||
|
||||
if self.collapse_join:
|
||||
line = line.lstrip()
|
||||
line = buildup_line + line
|
||||
|
||||
# careful: pay attention to line number when incrementing it
|
||||
if isinstance(self.current_line, list):
|
||||
self.current_line[1] = self.current_line[1] + 1
|
||||
else:
|
||||
self.current_line = [self.current_line, self.current_line + 1]
|
||||
# just an ordinary line, read it as usual
|
||||
else:
|
||||
if line is None: # eof
|
||||
return None
|
||||
|
||||
# still have to be careful about incrementing the line number!
|
||||
if isinstance(self.current_line, list):
|
||||
self.current_line = self.current_line[1] + 1
|
||||
else:
|
||||
self.current_line = self.current_line + 1
|
||||
|
||||
# strip whitespace however the client wants (leading and
|
||||
# trailing, or one or the other, or neither)
|
||||
if self.lstrip_ws and self.rstrip_ws:
|
||||
line = line.strip()
|
||||
elif self.lstrip_ws:
|
||||
line = line.lstrip()
|
||||
elif self.rstrip_ws:
|
||||
line = line.rstrip()
|
||||
|
||||
# blank line (whether we rstrip'ed or not)? skip to next line
|
||||
# if appropriate
|
||||
if line in ('', '\n') and self.skip_blanks:
|
||||
continue
|
||||
|
||||
if self.join_lines:
|
||||
if line[-1] == '\\':
|
||||
buildup_line = line[:-1]
|
||||
continue
|
||||
|
||||
if line[-2:] == '\\\n':
|
||||
buildup_line = line[0:-2] + '\n'
|
||||
continue
|
||||
|
||||
# well, I guess there's some actual content there: return it
|
||||
return line
|
||||
|
||||
def readlines(self):
|
||||
"""Read and return the list of all logical lines remaining in the
|
||||
current file."""
|
||||
lines = []
|
||||
while True:
|
||||
line = self.readline()
|
||||
if line is None:
|
||||
return lines
|
||||
lines.append(line)
|
||||
|
||||
def unreadline(self, line):
|
||||
"""Push 'line' (a string) onto an internal buffer that will be
|
||||
checked by future 'readline()' calls. Handy for implementing
|
||||
a parser with line-at-a-time lookahead."""
|
||||
self.linebuf.append(line)
|
||||
@@ -0,0 +1,9 @@
|
||||
import importlib
|
||||
|
||||
from .compilers.C import unix
|
||||
|
||||
UnixCCompiler = unix.Compiler
|
||||
|
||||
# ensure import of unixccompiler implies ccompiler imported
|
||||
# (pypa/setuptools#4871)
|
||||
importlib.import_module('distutils.ccompiler')
|
||||
518
.venv/Lib/site-packages/setuptools/_distutils/util.py
Normal file
518
.venv/Lib/site-packages/setuptools/_distutils/util.py
Normal file
@@ -0,0 +1,518 @@
|
||||
"""distutils.util
|
||||
|
||||
Miscellaneous utility functions -- anything that doesn't fit into
|
||||
one of the other *util.py modules.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import importlib.util
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import TYPE_CHECKING, AnyStr
|
||||
|
||||
from jaraco.functools import pass_none
|
||||
|
||||
from ._log import log
|
||||
from ._modified import newer
|
||||
from .errors import DistutilsByteCompileError, DistutilsPlatformError
|
||||
from .spawn import spawn
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
_Ts = TypeVarTuple("_Ts")
|
||||
|
||||
|
||||
def get_host_platform() -> str:
|
||||
"""
|
||||
Return a string that identifies the current platform. Use this
|
||||
function to distinguish platform-specific build directories and
|
||||
platform-specific built distributions.
|
||||
"""
|
||||
|
||||
# This function initially exposed platforms as defined in Python 3.9
|
||||
# even with older Python versions when distutils was split out.
|
||||
# Now it delegates to stdlib sysconfig.
|
||||
|
||||
return sysconfig.get_platform()
|
||||
|
||||
|
||||
def get_platform() -> str:
|
||||
if os.name == 'nt':
|
||||
TARGET_TO_PLAT = {
|
||||
'x86': 'win32',
|
||||
'x64': 'win-amd64',
|
||||
'arm': 'win-arm32',
|
||||
'arm64': 'win-arm64',
|
||||
}
|
||||
target = os.environ.get('VSCMD_ARG_TGT_ARCH')
|
||||
return TARGET_TO_PLAT.get(target) or get_host_platform()
|
||||
return get_host_platform()
|
||||
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
_syscfg_macosx_ver = None # cache the version pulled from sysconfig
|
||||
MACOSX_VERSION_VAR = 'MACOSX_DEPLOYMENT_TARGET'
|
||||
|
||||
|
||||
def _clear_cached_macosx_ver():
|
||||
"""For testing only. Do not call."""
|
||||
global _syscfg_macosx_ver
|
||||
_syscfg_macosx_ver = None
|
||||
|
||||
|
||||
def get_macosx_target_ver_from_syscfg():
|
||||
"""Get the version of macOS latched in the Python interpreter configuration.
|
||||
Returns the version as a string or None if can't obtain one. Cached."""
|
||||
global _syscfg_macosx_ver
|
||||
if _syscfg_macosx_ver is None:
|
||||
from distutils import sysconfig
|
||||
|
||||
ver = sysconfig.get_config_var(MACOSX_VERSION_VAR) or ''
|
||||
if ver:
|
||||
_syscfg_macosx_ver = ver
|
||||
return _syscfg_macosx_ver
|
||||
|
||||
|
||||
def get_macosx_target_ver():
|
||||
"""Return the version of macOS for which we are building.
|
||||
|
||||
The target version defaults to the version in sysconfig latched at time
|
||||
the Python interpreter was built, unless overridden by an environment
|
||||
variable. If neither source has a value, then None is returned"""
|
||||
|
||||
syscfg_ver = get_macosx_target_ver_from_syscfg()
|
||||
env_ver = os.environ.get(MACOSX_VERSION_VAR)
|
||||
|
||||
if env_ver:
|
||||
# Validate overridden version against sysconfig version, if have both.
|
||||
# Ensure that the deployment target of the build process is not less
|
||||
# than 10.3 if the interpreter was built for 10.3 or later. This
|
||||
# ensures extension modules are built with correct compatibility
|
||||
# values, specifically LDSHARED which can use
|
||||
# '-undefined dynamic_lookup' which only works on >= 10.3.
|
||||
if (
|
||||
syscfg_ver
|
||||
and split_version(syscfg_ver) >= [10, 3]
|
||||
and split_version(env_ver) < [10, 3]
|
||||
):
|
||||
my_msg = (
|
||||
'$' + MACOSX_VERSION_VAR + ' mismatch: '
|
||||
f'now "{env_ver}" but "{syscfg_ver}" during configure; '
|
||||
'must use 10.3 or later'
|
||||
)
|
||||
raise DistutilsPlatformError(my_msg)
|
||||
return env_ver
|
||||
return syscfg_ver
|
||||
|
||||
|
||||
def split_version(s: str) -> list[int]:
|
||||
"""Convert a dot-separated string into a list of numbers for comparisons"""
|
||||
return [int(n) for n in s.split('.')]
|
||||
|
||||
|
||||
@pass_none
|
||||
def convert_path(pathname: str | os.PathLike[str]) -> str:
|
||||
r"""
|
||||
Allow for pathlib.Path inputs, coax to a native path string.
|
||||
|
||||
If None is passed, will just pass it through as
|
||||
Setuptools relies on this behavior.
|
||||
|
||||
>>> convert_path(None) is None
|
||||
True
|
||||
|
||||
Removes empty paths.
|
||||
|
||||
>>> convert_path('foo/./bar').replace('\\', '/')
|
||||
'foo/bar'
|
||||
"""
|
||||
return os.fspath(pathlib.PurePath(pathname))
|
||||
|
||||
|
||||
def change_root(
|
||||
new_root: AnyStr | os.PathLike[AnyStr], pathname: AnyStr | os.PathLike[AnyStr]
|
||||
) -> AnyStr:
|
||||
"""Return 'pathname' with 'new_root' prepended. If 'pathname' is
|
||||
relative, this is equivalent to "os.path.join(new_root,pathname)".
|
||||
Otherwise, it requires making 'pathname' relative and then joining the
|
||||
two, which is tricky on DOS/Windows and Mac OS.
|
||||
"""
|
||||
if os.name == 'posix':
|
||||
if not os.path.isabs(pathname):
|
||||
return os.path.join(new_root, pathname)
|
||||
else:
|
||||
return os.path.join(new_root, pathname[1:])
|
||||
|
||||
elif os.name == 'nt':
|
||||
(drive, path) = os.path.splitdrive(pathname)
|
||||
if path[0] == os.sep:
|
||||
path = path[1:]
|
||||
return os.path.join(new_root, path)
|
||||
|
||||
raise DistutilsPlatformError(f"nothing known about platform '{os.name}'")
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def check_environ() -> None:
|
||||
"""Ensure that 'os.environ' has all the environment variables we
|
||||
guarantee that users can use in config files, command-line options,
|
||||
etc. Currently this includes:
|
||||
HOME - user's home directory (Unix only)
|
||||
PLAT - description of the current platform, including hardware
|
||||
and OS (see 'get_platform()')
|
||||
"""
|
||||
if os.name == 'posix' and 'HOME' not in os.environ:
|
||||
try:
|
||||
import pwd
|
||||
|
||||
os.environ['HOME'] = pwd.getpwuid(os.getuid())[5]
|
||||
except (ImportError, KeyError):
|
||||
# bpo-10496: if the current user identifier doesn't exist in the
|
||||
# password database, do nothing
|
||||
pass
|
||||
|
||||
if 'PLAT' not in os.environ:
|
||||
os.environ['PLAT'] = get_platform()
|
||||
|
||||
|
||||
def subst_vars(s, local_vars: Mapping[str, object]) -> str:
|
||||
"""
|
||||
Perform variable substitution on 'string'.
|
||||
Variables are indicated by format-style braces ("{var}").
|
||||
Variable is substituted by the value found in the 'local_vars'
|
||||
dictionary or in 'os.environ' if it's not in 'local_vars'.
|
||||
'os.environ' is first checked/augmented to guarantee that it contains
|
||||
certain values: see 'check_environ()'. Raise ValueError for any
|
||||
variables not found in either 'local_vars' or 'os.environ'.
|
||||
"""
|
||||
check_environ()
|
||||
lookup = dict(os.environ)
|
||||
lookup.update((name, str(value)) for name, value in local_vars.items())
|
||||
try:
|
||||
return _subst_compat(s).format_map(lookup)
|
||||
except KeyError as var:
|
||||
raise ValueError(f"invalid variable {var}")
|
||||
|
||||
|
||||
def _subst_compat(s):
|
||||
"""
|
||||
Replace shell/Perl-style variable substitution with
|
||||
format-style. For compatibility.
|
||||
"""
|
||||
|
||||
def _subst(match):
|
||||
return f'{{{match.group(1)}}}'
|
||||
|
||||
repl = re.sub(r'\$([a-zA-Z_][a-zA-Z_0-9]*)', _subst, s)
|
||||
if repl != s:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"shell/Perl-style substitutions are deprecated",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return repl
|
||||
|
||||
|
||||
def grok_environment_error(exc: object, prefix: str = "error: ") -> str:
|
||||
# Function kept for backward compatibility.
|
||||
# Used to try clever things with EnvironmentErrors,
|
||||
# but nowadays str(exception) produces good messages.
|
||||
return prefix + str(exc)
|
||||
|
||||
|
||||
# Needed by 'split_quoted()'
|
||||
_wordchars_re = _squote_re = _dquote_re = None
|
||||
|
||||
|
||||
def _init_regex():
|
||||
global _wordchars_re, _squote_re, _dquote_re
|
||||
_wordchars_re = re.compile(rf'[^\\\'\"{string.whitespace} ]*')
|
||||
_squote_re = re.compile(r"'(?:[^'\\]|\\.)*'")
|
||||
_dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"')
|
||||
|
||||
|
||||
def split_quoted(s: str) -> list[str]:
|
||||
"""Split a string up according to Unix shell-like rules for quotes and
|
||||
backslashes. In short: words are delimited by spaces, as long as those
|
||||
spaces are not escaped by a backslash, or inside a quoted string.
|
||||
Single and double quotes are equivalent, and the quote characters can
|
||||
be backslash-escaped. The backslash is stripped from any two-character
|
||||
escape sequence, leaving only the escaped character. The quote
|
||||
characters are stripped from any quoted string. Returns a list of
|
||||
words.
|
||||
"""
|
||||
|
||||
# This is a nice algorithm for splitting up a single string, since it
|
||||
# doesn't require character-by-character examination. It was a little
|
||||
# bit of a brain-bender to get it working right, though...
|
||||
if _wordchars_re is None:
|
||||
_init_regex()
|
||||
|
||||
s = s.strip()
|
||||
words = []
|
||||
pos = 0
|
||||
|
||||
while s:
|
||||
m = _wordchars_re.match(s, pos)
|
||||
end = m.end()
|
||||
if end == len(s):
|
||||
words.append(s[:end])
|
||||
break
|
||||
|
||||
if s[end] in string.whitespace:
|
||||
# unescaped, unquoted whitespace: now
|
||||
# we definitely have a word delimiter
|
||||
words.append(s[:end])
|
||||
s = s[end:].lstrip()
|
||||
pos = 0
|
||||
|
||||
elif s[end] == '\\':
|
||||
# preserve whatever is being escaped;
|
||||
# will become part of the current word
|
||||
s = s[:end] + s[end + 1 :]
|
||||
pos = end + 1
|
||||
|
||||
else:
|
||||
if s[end] == "'": # slurp singly-quoted string
|
||||
m = _squote_re.match(s, end)
|
||||
elif s[end] == '"': # slurp doubly-quoted string
|
||||
m = _dquote_re.match(s, end)
|
||||
else:
|
||||
raise RuntimeError(f"this can't happen (bad char '{s[end]}')")
|
||||
|
||||
if m is None:
|
||||
raise ValueError(f"bad string (mismatched {s[end]} quotes?)")
|
||||
|
||||
(beg, end) = m.span()
|
||||
s = s[:beg] + s[beg + 1 : end - 1] + s[end:]
|
||||
pos = m.end() - 2
|
||||
|
||||
if pos >= len(s):
|
||||
words.append(s)
|
||||
break
|
||||
|
||||
return words
|
||||
|
||||
|
||||
# split_quoted ()
|
||||
|
||||
|
||||
def execute(
|
||||
func: Callable[[Unpack[_Ts]], object],
|
||||
args: tuple[Unpack[_Ts]],
|
||||
msg: object = None,
|
||||
verbose: bool = False,
|
||||
dry_run: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Perform some action that affects the outside world (e.g. by
|
||||
writing to the filesystem). Such actions are special because they
|
||||
are disabled by the 'dry_run' flag. This method handles that
|
||||
complication; simply supply the
|
||||
function to call and an argument tuple for it (to embody the
|
||||
"external action" being performed) and an optional message to
|
||||
emit.
|
||||
"""
|
||||
if msg is None:
|
||||
msg = f"{func.__name__}{args!r}"
|
||||
if msg[-2:] == ',)': # correct for singleton tuple
|
||||
msg = msg[0:-2] + ')'
|
||||
|
||||
log.info(msg)
|
||||
if not dry_run:
|
||||
func(*args)
|
||||
|
||||
|
||||
def strtobool(val: str) -> bool:
|
||||
"""Convert a string representation of truth to true (1) or false (0).
|
||||
|
||||
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
|
||||
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
|
||||
'val' is anything else.
|
||||
"""
|
||||
val = val.lower()
|
||||
if val in ('y', 'yes', 't', 'true', 'on', '1'):
|
||||
return True
|
||||
elif val in ('n', 'no', 'f', 'false', 'off', '0'):
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"invalid truth value {val!r}")
|
||||
|
||||
|
||||
def byte_compile( # noqa: C901
|
||||
py_files: Iterable[str],
|
||||
optimize: int = 0,
|
||||
force: bool = False,
|
||||
prefix: str | None = None,
|
||||
base_dir: str | None = None,
|
||||
verbose: bool = True,
|
||||
dry_run: bool = False,
|
||||
direct: bool | None = None,
|
||||
) -> None:
|
||||
"""Byte-compile a collection of Python source files to .pyc
|
||||
files in a __pycache__ subdirectory. 'py_files' is a list
|
||||
of files to compile; any files that don't end in ".py" are silently
|
||||
skipped. 'optimize' must be one of the following:
|
||||
0 - don't optimize
|
||||
1 - normal optimization (like "python -O")
|
||||
2 - extra optimization (like "python -OO")
|
||||
If 'force' is true, all files are recompiled regardless of
|
||||
timestamps.
|
||||
|
||||
The source filename encoded in each bytecode file defaults to the
|
||||
filenames listed in 'py_files'; you can modify these with 'prefix' and
|
||||
'basedir'. 'prefix' is a string that will be stripped off of each
|
||||
source filename, and 'base_dir' is a directory name that will be
|
||||
prepended (after 'prefix' is stripped). You can supply either or both
|
||||
(or neither) of 'prefix' and 'base_dir', as you wish.
|
||||
|
||||
If 'dry_run' is true, doesn't actually do anything that would
|
||||
affect the filesystem.
|
||||
|
||||
Byte-compilation is either done directly in this interpreter process
|
||||
with the standard py_compile module, or indirectly by writing a
|
||||
temporary script and executing it. Normally, you should let
|
||||
'byte_compile()' figure out to use direct compilation or not (see
|
||||
the source for details). The 'direct' flag is used by the script
|
||||
generated in indirect mode; unless you know what you're doing, leave
|
||||
it set to None.
|
||||
"""
|
||||
|
||||
# nothing is done if sys.dont_write_bytecode is True
|
||||
if sys.dont_write_bytecode:
|
||||
raise DistutilsByteCompileError('byte-compiling is disabled.')
|
||||
|
||||
# First, if the caller didn't force us into direct or indirect mode,
|
||||
# figure out which mode we should be in. We take a conservative
|
||||
# approach: choose direct mode *only* if the current interpreter is
|
||||
# in debug mode and optimize is 0. If we're not in debug mode (-O
|
||||
# or -OO), we don't know which level of optimization this
|
||||
# interpreter is running with, so we can't do direct
|
||||
# byte-compilation and be certain that it's the right thing. Thus,
|
||||
# always compile indirectly if the current interpreter is in either
|
||||
# optimize mode, or if either optimization level was requested by
|
||||
# the caller.
|
||||
if direct is None:
|
||||
direct = __debug__ and optimize == 0
|
||||
|
||||
# "Indirect" byte-compilation: write a temporary script and then
|
||||
# run it with the appropriate flags.
|
||||
if not direct:
|
||||
(script_fd, script_name) = tempfile.mkstemp(".py")
|
||||
log.info("writing byte-compilation script '%s'", script_name)
|
||||
if not dry_run:
|
||||
script = os.fdopen(script_fd, "w", encoding='utf-8')
|
||||
|
||||
with script:
|
||||
script.write(
|
||||
"""\
|
||||
from distutils.util import byte_compile
|
||||
files = [
|
||||
"""
|
||||
)
|
||||
|
||||
# XXX would be nice to write absolute filenames, just for
|
||||
# safety's sake (script should be more robust in the face of
|
||||
# chdir'ing before running it). But this requires abspath'ing
|
||||
# 'prefix' as well, and that breaks the hack in build_lib's
|
||||
# 'byte_compile()' method that carefully tacks on a trailing
|
||||
# slash (os.sep really) to make sure the prefix here is "just
|
||||
# right". This whole prefix business is rather delicate -- the
|
||||
# problem is that it's really a directory, but I'm treating it
|
||||
# as a dumb string, so trailing slashes and so forth matter.
|
||||
|
||||
script.write(",\n".join(map(repr, py_files)) + "]\n")
|
||||
script.write(
|
||||
f"""
|
||||
byte_compile(files, optimize={optimize!r}, force={force!r},
|
||||
prefix={prefix!r}, base_dir={base_dir!r},
|
||||
verbose={verbose!r}, dry_run=False,
|
||||
direct=True)
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = [sys.executable]
|
||||
cmd.extend(subprocess._optim_args_from_interpreter_flags())
|
||||
cmd.append(script_name)
|
||||
spawn(cmd, dry_run=dry_run)
|
||||
execute(os.remove, (script_name,), f"removing {script_name}", dry_run=dry_run)
|
||||
|
||||
# "Direct" byte-compilation: use the py_compile module to compile
|
||||
# right here, right now. Note that the script generated in indirect
|
||||
# mode simply calls 'byte_compile()' in direct mode, a weird sort of
|
||||
# cross-process recursion. Hey, it works!
|
||||
else:
|
||||
from py_compile import compile
|
||||
|
||||
for file in py_files:
|
||||
if file[-3:] != ".py":
|
||||
# This lets us be lazy and not filter filenames in
|
||||
# the "install_lib" command.
|
||||
continue
|
||||
|
||||
# Terminology from the py_compile module:
|
||||
# cfile - byte-compiled file
|
||||
# dfile - purported source filename (same as 'file' by default)
|
||||
if optimize >= 0:
|
||||
opt = '' if optimize == 0 else optimize
|
||||
cfile = importlib.util.cache_from_source(file, optimization=opt)
|
||||
else:
|
||||
cfile = importlib.util.cache_from_source(file)
|
||||
dfile = file
|
||||
if prefix:
|
||||
if file[: len(prefix)] != prefix:
|
||||
raise ValueError(
|
||||
f"invalid prefix: filename {file!r} doesn't start with {prefix!r}"
|
||||
)
|
||||
dfile = dfile[len(prefix) :]
|
||||
if base_dir:
|
||||
dfile = os.path.join(base_dir, dfile)
|
||||
|
||||
cfile_base = os.path.basename(cfile)
|
||||
if direct:
|
||||
if force or newer(file, cfile):
|
||||
log.info("byte-compiling %s to %s", file, cfile_base)
|
||||
if not dry_run:
|
||||
compile(file, cfile, dfile)
|
||||
else:
|
||||
log.debug("skipping byte-compilation of %s to %s", file, cfile_base)
|
||||
|
||||
|
||||
def rfc822_escape(header: str) -> str:
|
||||
"""Return a version of the string escaped for inclusion in an
|
||||
RFC-822 header, by ensuring there are 8 spaces space after each newline.
|
||||
"""
|
||||
indent = 8 * " "
|
||||
lines = header.splitlines(keepends=True)
|
||||
|
||||
# Emulate the behaviour of `str.split`
|
||||
# (the terminal line break in `splitlines` does not result in an extra line):
|
||||
ends_in_newline = lines and lines[-1].splitlines()[0] != lines[-1]
|
||||
suffix = indent if ends_in_newline else ""
|
||||
|
||||
return indent.join(lines) + suffix
|
||||
|
||||
|
||||
def is_mingw() -> bool:
|
||||
"""Returns True if the current platform is mingw.
|
||||
|
||||
Python compiled with Mingw-w64 has sys.platform == 'win32' and
|
||||
get_platform() starts with 'mingw'.
|
||||
"""
|
||||
return sys.platform == 'win32' and get_platform().startswith('mingw')
|
||||
|
||||
|
||||
def is_freethreaded():
|
||||
"""Return True if the Python interpreter is built with free threading support."""
|
||||
return bool(sysconfig.get_config_var('Py_GIL_DISABLED'))
|
||||
348
.venv/Lib/site-packages/setuptools/_distutils/version.py
Normal file
348
.venv/Lib/site-packages/setuptools/_distutils/version.py
Normal file
@@ -0,0 +1,348 @@
|
||||
#
|
||||
# distutils/version.py
|
||||
#
|
||||
# Implements multiple version numbering conventions for the
|
||||
# Python Module Distribution Utilities.
|
||||
#
|
||||
# $Id$
|
||||
#
|
||||
|
||||
"""Provides classes to represent module version numbers (one class for
|
||||
each style of version numbering). There are currently two such classes
|
||||
implemented: StrictVersion and LooseVersion.
|
||||
|
||||
Every version number class implements the following interface:
|
||||
* the 'parse' method takes a string and parses it to some internal
|
||||
representation; if the string is an invalid version number,
|
||||
'parse' raises a ValueError exception
|
||||
* the class constructor takes an optional string argument which,
|
||||
if supplied, is passed to 'parse'
|
||||
* __str__ reconstructs the string that was passed to 'parse' (or
|
||||
an equivalent string -- ie. one that will generate an equivalent
|
||||
version number instance)
|
||||
* __repr__ generates Python code to recreate the version number instance
|
||||
* _cmp compares the current instance with either another instance
|
||||
of the same class or a string (which will be parsed to an instance
|
||||
of the same class, thus must follow the same rules)
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import warnings
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_known_deprecation():
|
||||
with warnings.catch_warnings(record=True) as ctx:
|
||||
warnings.filterwarnings(
|
||||
action='default',
|
||||
category=DeprecationWarning,
|
||||
message="distutils Version classes are deprecated.",
|
||||
)
|
||||
yield ctx
|
||||
|
||||
|
||||
class Version:
|
||||
"""Abstract base class for version numbering classes. Just provides
|
||||
constructor (__init__) and reproducer (__repr__), because those
|
||||
seem to be the same for all version numbering classes; and route
|
||||
rich comparisons to _cmp.
|
||||
"""
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
warnings.warn(
|
||||
"distutils Version classes are deprecated. Use packaging.version instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__} ('{self}')"
|
||||
|
||||
def __eq__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c == 0
|
||||
|
||||
def __lt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c < 0
|
||||
|
||||
def __le__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c <= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c > 0
|
||||
|
||||
def __ge__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c >= 0
|
||||
|
||||
|
||||
# Interface for version-number classes -- must be implemented
|
||||
# by the following classes (the concrete ones -- Version should
|
||||
# be treated as an abstract class).
|
||||
# __init__ (string) - create and take same action as 'parse'
|
||||
# (string parameter is optional)
|
||||
# parse (string) - convert a string representation to whatever
|
||||
# internal representation is appropriate for
|
||||
# this style of version numbering
|
||||
# __str__ (self) - convert back to a string; should be very similar
|
||||
# (if not identical to) the string supplied to parse
|
||||
# __repr__ (self) - generate Python code to recreate
|
||||
# the instance
|
||||
# _cmp (self, other) - compare two version numbers ('other' may
|
||||
# be an unparsed version string, or another
|
||||
# instance of your version class)
|
||||
|
||||
|
||||
class StrictVersion(Version):
|
||||
"""Version numbering for anal retentives and software idealists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of two or three
|
||||
dot-separated numeric components, with an optional "pre-release" tag
|
||||
on the end. The pre-release tag consists of the letter 'a' or 'b'
|
||||
followed by a number. If the numeric components of two version
|
||||
numbers are equal, then one with a pre-release tag will always
|
||||
be deemed earlier (lesser) than one without.
|
||||
|
||||
The following are valid version numbers (shown in the order that
|
||||
would be obtained by sorting according to the supplied cmp function):
|
||||
|
||||
0.4 0.4.0 (these two are equivalent)
|
||||
0.4.1
|
||||
0.5a1
|
||||
0.5b3
|
||||
0.5
|
||||
0.9.6
|
||||
1.0
|
||||
1.0.4a3
|
||||
1.0.4b1
|
||||
1.0.4
|
||||
|
||||
The following are examples of invalid version numbers:
|
||||
|
||||
1
|
||||
2.7.2.2
|
||||
1.3.a4
|
||||
1.3pl1
|
||||
1.3c4
|
||||
|
||||
The rationale for this version numbering system will be explained
|
||||
in the distutils documentation.
|
||||
"""
|
||||
|
||||
version_re = re.compile(
|
||||
r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', re.VERBOSE | re.ASCII
|
||||
)
|
||||
|
||||
def parse(self, vstring):
|
||||
match = self.version_re.match(vstring)
|
||||
if not match:
|
||||
raise ValueError(f"invalid version number '{vstring}'")
|
||||
|
||||
(major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6)
|
||||
|
||||
if patch:
|
||||
self.version = tuple(map(int, [major, minor, patch]))
|
||||
else:
|
||||
self.version = tuple(map(int, [major, minor])) + (0,)
|
||||
|
||||
if prerelease:
|
||||
self.prerelease = (prerelease[0], int(prerelease_num))
|
||||
else:
|
||||
self.prerelease = None
|
||||
|
||||
def __str__(self):
|
||||
if self.version[2] == 0:
|
||||
vstring = '.'.join(map(str, self.version[0:2]))
|
||||
else:
|
||||
vstring = '.'.join(map(str, self.version))
|
||||
|
||||
if self.prerelease:
|
||||
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
|
||||
|
||||
return vstring
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
with suppress_known_deprecation():
|
||||
other = StrictVersion(other)
|
||||
elif not isinstance(other, StrictVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version == other.version:
|
||||
# versions match; pre-release drives the comparison
|
||||
return self._cmp_prerelease(other)
|
||||
|
||||
return -1 if self.version < other.version else 1
|
||||
|
||||
def _cmp_prerelease(self, other):
|
||||
"""
|
||||
case 1: self has prerelease, other doesn't; other is greater
|
||||
case 2: self doesn't have prerelease, other does: self is greater
|
||||
case 3: both or neither have prerelease: compare them!
|
||||
"""
|
||||
if self.prerelease and not other.prerelease:
|
||||
return -1
|
||||
elif not self.prerelease and other.prerelease:
|
||||
return 1
|
||||
|
||||
if self.prerelease == other.prerelease:
|
||||
return 0
|
||||
elif self.prerelease < other.prerelease:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
# end class StrictVersion
|
||||
|
||||
|
||||
# The rules according to Greg Stein:
|
||||
# 1) a version number has 1 or more numbers separated by a period or by
|
||||
# sequences of letters. If only periods, then these are compared
|
||||
# left-to-right to determine an ordering.
|
||||
# 2) sequences of letters are part of the tuple for comparison and are
|
||||
# compared lexicographically
|
||||
# 3) recognize the numeric components may have leading zeroes
|
||||
#
|
||||
# The LooseVersion class below implements these rules: a version number
|
||||
# string is split up into a tuple of integer and string components, and
|
||||
# comparison is a simple tuple comparison. This means that version
|
||||
# numbers behave in a predictable and obvious way, but a way that might
|
||||
# not necessarily be how people *want* version numbers to behave. There
|
||||
# wouldn't be a problem if people could stick to purely numeric version
|
||||
# numbers: just split on period and compare the numbers as tuples.
|
||||
# However, people insist on putting letters into their version numbers;
|
||||
# the most common purpose seems to be:
|
||||
# - indicating a "pre-release" version
|
||||
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
|
||||
# - indicating a post-release patch ('p', 'pl', 'patch')
|
||||
# but of course this can't cover all version number schemes, and there's
|
||||
# no way to know what a programmer means without asking him.
|
||||
#
|
||||
# The problem is what to do with letters (and other non-numeric
|
||||
# characters) in a version number. The current implementation does the
|
||||
# obvious and predictable thing: keep them as strings and compare
|
||||
# lexically within a tuple comparison. This has the desired effect if
|
||||
# an appended letter sequence implies something "post-release":
|
||||
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
|
||||
#
|
||||
# However, if letters in a version number imply a pre-release version,
|
||||
# the "obvious" thing isn't correct. Eg. you would expect that
|
||||
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
|
||||
# implemented here, this just isn't so.
|
||||
#
|
||||
# Two possible solutions come to mind. The first is to tie the
|
||||
# comparison algorithm to a particular set of semantic rules, as has
|
||||
# been done in the StrictVersion class above. This works great as long
|
||||
# as everyone can go along with bondage and discipline. Hopefully a
|
||||
# (large) subset of Python module programmers will agree that the
|
||||
# particular flavour of bondage and discipline provided by StrictVersion
|
||||
# provides enough benefit to be worth using, and will submit their
|
||||
# version numbering scheme to its domination. The free-thinking
|
||||
# anarchists in the lot will never give in, though, and something needs
|
||||
# to be done to accommodate them.
|
||||
#
|
||||
# Perhaps a "moderately strict" version class could be implemented that
|
||||
# lets almost anything slide (syntactically), and makes some heuristic
|
||||
# assumptions about non-digits in version number strings. This could
|
||||
# sink into special-case-hell, though; if I was as talented and
|
||||
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
|
||||
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
|
||||
# just as happy dealing with things like "2g6" and "1.13++". I don't
|
||||
# think I'm smart enough to do it right though.
|
||||
#
|
||||
# In any case, I've coded the test suite for this module (see
|
||||
# ../test/test_version.py) specifically to fail on things like comparing
|
||||
# "1.2a2" and "1.2". That's not because the *code* is doing anything
|
||||
# wrong, it's because the simple, obvious design doesn't match my
|
||||
# complicated, hairy expectations for real-world version numbers. It
|
||||
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
|
||||
# the Right Thing" (ie. the code matches the conception). But I'd rather
|
||||
# have a conception that matches common notions about version numbers.
|
||||
|
||||
|
||||
class LooseVersion(Version):
|
||||
"""Version numbering for anarchists and software realists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of a series of numbers,
|
||||
separated by either periods or strings of letters. When comparing
|
||||
version numbers, the numeric components will be compared
|
||||
numerically, and the alphabetic components lexically. The following
|
||||
are all valid version numbers, in no particular order:
|
||||
|
||||
1.5.1
|
||||
1.5.2b2
|
||||
161
|
||||
3.10a
|
||||
8.02
|
||||
3.4j
|
||||
1996.07.12
|
||||
3.2.pl0
|
||||
3.1.1.6
|
||||
2g6
|
||||
11g
|
||||
0.960923
|
||||
2.2beta29
|
||||
1.13++
|
||||
5.5.kw
|
||||
2.0b1pl0
|
||||
|
||||
In fact, there is no such thing as an invalid version number under
|
||||
this scheme; the rules for comparison are simple and predictable,
|
||||
but may not always give the results you want (for some definition
|
||||
of "want").
|
||||
"""
|
||||
|
||||
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
|
||||
|
||||
def parse(self, vstring):
|
||||
# I've given up on thinking I can reconstruct the version string
|
||||
# from the parsed tuple -- so I just store the string here for
|
||||
# use by __str__
|
||||
self.vstring = vstring
|
||||
components = [x for x in self.component_re.split(vstring) if x and x != '.']
|
||||
for i, obj in enumerate(components):
|
||||
try:
|
||||
components[i] = int(obj)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.version = components
|
||||
|
||||
def __str__(self):
|
||||
return self.vstring
|
||||
|
||||
def __repr__(self):
|
||||
return f"LooseVersion ('{self}')"
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = LooseVersion(other)
|
||||
elif not isinstance(other, LooseVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version == other.version:
|
||||
return 0
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
if self.version > other.version:
|
||||
return 1
|
||||
|
||||
|
||||
# end class LooseVersion
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Module for parsing and testing package version predicate strings."""
|
||||
|
||||
import operator
|
||||
import re
|
||||
|
||||
from . import version
|
||||
|
||||
re_validPackage = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)", re.ASCII)
|
||||
# (package) (rest)
|
||||
|
||||
re_paren = re.compile(r"^\s*\((.*)\)\s*$") # (list) inside of parentheses
|
||||
re_splitComparison = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$")
|
||||
# (comp) (version)
|
||||
|
||||
|
||||
def splitUp(pred):
|
||||
"""Parse a single version comparison.
|
||||
|
||||
Return (comparison string, StrictVersion)
|
||||
"""
|
||||
res = re_splitComparison.match(pred)
|
||||
if not res:
|
||||
raise ValueError(f"bad package restriction syntax: {pred!r}")
|
||||
comp, verStr = res.groups()
|
||||
with version.suppress_known_deprecation():
|
||||
other = version.StrictVersion(verStr)
|
||||
return (comp, other)
|
||||
|
||||
|
||||
compmap = {
|
||||
"<": operator.lt,
|
||||
"<=": operator.le,
|
||||
"==": operator.eq,
|
||||
">": operator.gt,
|
||||
">=": operator.ge,
|
||||
"!=": operator.ne,
|
||||
}
|
||||
|
||||
|
||||
class VersionPredicate:
|
||||
"""Parse and test package version predicates.
|
||||
|
||||
>>> v = VersionPredicate('pyepat.abc (>1.0, <3333.3a1, !=1555.1b3)')
|
||||
|
||||
The `name` attribute provides the full dotted name that is given::
|
||||
|
||||
>>> v.name
|
||||
'pyepat.abc'
|
||||
|
||||
The str() of a `VersionPredicate` provides a normalized
|
||||
human-readable version of the expression::
|
||||
|
||||
>>> print(v)
|
||||
pyepat.abc (> 1.0, < 3333.3a1, != 1555.1b3)
|
||||
|
||||
The `satisfied_by()` method can be used to determine with a given
|
||||
version number is included in the set described by the version
|
||||
restrictions::
|
||||
|
||||
>>> v.satisfied_by('1.1')
|
||||
True
|
||||
>>> v.satisfied_by('1.4')
|
||||
True
|
||||
>>> v.satisfied_by('1.0')
|
||||
False
|
||||
>>> v.satisfied_by('4444.4')
|
||||
False
|
||||
>>> v.satisfied_by('1555.1b3')
|
||||
False
|
||||
|
||||
`VersionPredicate` is flexible in accepting extra whitespace::
|
||||
|
||||
>>> v = VersionPredicate(' pat( == 0.1 ) ')
|
||||
>>> v.name
|
||||
'pat'
|
||||
>>> v.satisfied_by('0.1')
|
||||
True
|
||||
>>> v.satisfied_by('0.2')
|
||||
False
|
||||
|
||||
If any version numbers passed in do not conform to the
|
||||
restrictions of `StrictVersion`, a `ValueError` is raised::
|
||||
|
||||
>>> v = VersionPredicate('p1.p2.p3.p4(>=1.0, <=1.3a1, !=1.2zb3)')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid version number '1.2zb3'
|
||||
|
||||
It the module or package name given does not conform to what's
|
||||
allowed as a legal module or package name, `ValueError` is
|
||||
raised::
|
||||
|
||||
>>> v = VersionPredicate('foo-bar')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: expected parenthesized list: '-bar'
|
||||
|
||||
>>> v = VersionPredicate('foo bar (12.21)')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: expected parenthesized list: 'bar (12.21)'
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, versionPredicateStr):
|
||||
"""Parse a version predicate string."""
|
||||
# Fields:
|
||||
# name: package name
|
||||
# pred: list of (comparison string, StrictVersion)
|
||||
|
||||
versionPredicateStr = versionPredicateStr.strip()
|
||||
if not versionPredicateStr:
|
||||
raise ValueError("empty package restriction")
|
||||
match = re_validPackage.match(versionPredicateStr)
|
||||
if not match:
|
||||
raise ValueError(f"bad package name in {versionPredicateStr!r}")
|
||||
self.name, paren = match.groups()
|
||||
paren = paren.strip()
|
||||
if paren:
|
||||
match = re_paren.match(paren)
|
||||
if not match:
|
||||
raise ValueError(f"expected parenthesized list: {paren!r}")
|
||||
str = match.groups()[0]
|
||||
self.pred = [splitUp(aPred) for aPred in str.split(",")]
|
||||
if not self.pred:
|
||||
raise ValueError(f"empty parenthesized list in {versionPredicateStr!r}")
|
||||
else:
|
||||
self.pred = []
|
||||
|
||||
def __str__(self):
|
||||
if self.pred:
|
||||
seq = [cond + " " + str(ver) for cond, ver in self.pred]
|
||||
return self.name + " (" + ", ".join(seq) + ")"
|
||||
else:
|
||||
return self.name
|
||||
|
||||
def satisfied_by(self, version):
|
||||
"""True if version is compatible with all the predicates in self.
|
||||
The parameter version must be acceptable to the StrictVersion
|
||||
constructor. It may be either a string or StrictVersion.
|
||||
"""
|
||||
for cond, ver in self.pred:
|
||||
if not compmap[cond](version, ver):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
_provision_rx = None
|
||||
|
||||
|
||||
def split_provision(value):
|
||||
"""Return the name and optional version number of a provision.
|
||||
|
||||
The version number, if given, will be returned as a `StrictVersion`
|
||||
instance, otherwise it will be `None`.
|
||||
|
||||
>>> split_provision('mypkg')
|
||||
('mypkg', None)
|
||||
>>> split_provision(' mypkg( 1.2 ) ')
|
||||
('mypkg', StrictVersion ('1.2'))
|
||||
"""
|
||||
global _provision_rx
|
||||
if _provision_rx is None:
|
||||
_provision_rx = re.compile(
|
||||
r"([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)(?:\s*\(\s*([^)\s]+)\s*\))?$", re.ASCII
|
||||
)
|
||||
value = value.strip()
|
||||
m = _provision_rx.match(value)
|
||||
if not m:
|
||||
raise ValueError(f"illegal provides specification: {value!r}")
|
||||
ver = m.group(2) or None
|
||||
if ver:
|
||||
with version.suppress_known_deprecation():
|
||||
ver = version.StrictVersion(ver)
|
||||
return m.group(1), ver
|
||||
@@ -0,0 +1,3 @@
|
||||
from .compilers.C import zos
|
||||
|
||||
zOSCCompiler = zos.Compiler
|
||||
90
.venv/Lib/site-packages/setuptools/_entry_points.py
Normal file
90
.venv/Lib/site-packages/setuptools/_entry_points.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import functools
|
||||
import itertools
|
||||
import operator
|
||||
|
||||
from jaraco.functools import pass_none
|
||||
from jaraco.text import yield_lines
|
||||
from more_itertools import consume
|
||||
|
||||
from ._importlib import metadata
|
||||
from ._itertools import ensure_unique
|
||||
from .errors import OptionError
|
||||
|
||||
|
||||
def ensure_valid(ep):
|
||||
"""
|
||||
Exercise one of the dynamic properties to trigger
|
||||
the pattern match.
|
||||
"""
|
||||
try:
|
||||
ep.extras
|
||||
except (AttributeError, AssertionError) as ex:
|
||||
# Why both? See https://github.com/python/importlib_metadata/issues/488
|
||||
msg = (
|
||||
f"Problems to parse {ep}.\nPlease ensure entry-point follows the spec: "
|
||||
"https://packaging.python.org/en/latest/specifications/entry-points/"
|
||||
)
|
||||
raise OptionError(msg) from ex
|
||||
|
||||
|
||||
def load_group(value, group):
|
||||
"""
|
||||
Given a value of an entry point or series of entry points,
|
||||
return each as an EntryPoint.
|
||||
"""
|
||||
# normalize to a single sequence of lines
|
||||
lines = yield_lines(value)
|
||||
text = f'[{group}]\n' + '\n'.join(lines)
|
||||
return metadata.EntryPoints._from_text(text)
|
||||
|
||||
|
||||
def by_group_and_name(ep):
|
||||
return ep.group, ep.name
|
||||
|
||||
|
||||
def validate(eps: metadata.EntryPoints):
|
||||
"""
|
||||
Ensure entry points are unique by group and name and validate each.
|
||||
"""
|
||||
consume(map(ensure_valid, ensure_unique(eps, key=by_group_and_name)))
|
||||
return eps
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
def load(eps):
|
||||
"""
|
||||
Given a Distribution.entry_points, produce EntryPoints.
|
||||
"""
|
||||
groups = itertools.chain.from_iterable(
|
||||
load_group(value, group) for group, value in eps.items()
|
||||
)
|
||||
return validate(metadata.EntryPoints(groups))
|
||||
|
||||
|
||||
@load.register(str)
|
||||
def _(eps):
|
||||
r"""
|
||||
>>> ep, = load('[console_scripts]\nfoo=bar')
|
||||
>>> ep.group
|
||||
'console_scripts'
|
||||
>>> ep.name
|
||||
'foo'
|
||||
>>> ep.value
|
||||
'bar'
|
||||
"""
|
||||
return validate(metadata.EntryPoints(metadata.EntryPoints._from_text(eps)))
|
||||
|
||||
|
||||
load.register(type(None), lambda x: x)
|
||||
|
||||
|
||||
@pass_none
|
||||
def render(eps: metadata.EntryPoints):
|
||||
by_group = operator.attrgetter('group')
|
||||
groups = itertools.groupby(sorted(eps, key=by_group), by_group)
|
||||
|
||||
return '\n'.join(f'[{group}]\n{render_items(items)}\n' for group, items in groups)
|
||||
|
||||
|
||||
def render_items(eps):
|
||||
return '\n'.join(f'{ep.name} = {ep.value}' for ep in sorted(eps))
|
||||
87
.venv/Lib/site-packages/setuptools/_imp.py
Normal file
87
.venv/Lib/site-packages/setuptools/_imp.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Re-implementation of find_module and get_frozen_object
|
||||
from the deprecated imp module.
|
||||
"""
|
||||
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import os
|
||||
import tokenize
|
||||
from importlib.util import module_from_spec
|
||||
|
||||
PY_SOURCE = 1
|
||||
PY_COMPILED = 2
|
||||
C_EXTENSION = 3
|
||||
C_BUILTIN = 6
|
||||
PY_FROZEN = 7
|
||||
|
||||
|
||||
def find_spec(module, paths):
|
||||
finder = (
|
||||
importlib.machinery.PathFinder().find_spec
|
||||
if isinstance(paths, list)
|
||||
else importlib.util.find_spec
|
||||
)
|
||||
return finder(module, paths)
|
||||
|
||||
|
||||
def find_module(module, paths=None):
|
||||
"""Just like 'imp.find_module()', but with package support"""
|
||||
spec = find_spec(module, paths)
|
||||
if spec is None:
|
||||
raise ImportError(f"Can't find {module}")
|
||||
if not spec.has_location and hasattr(spec, 'submodule_search_locations'):
|
||||
spec = importlib.util.spec_from_loader('__init__.py', spec.loader)
|
||||
|
||||
kind = -1
|
||||
file = None
|
||||
static = isinstance(spec.loader, type)
|
||||
if (
|
||||
spec.origin == 'frozen'
|
||||
or static
|
||||
and issubclass(spec.loader, importlib.machinery.FrozenImporter)
|
||||
):
|
||||
kind = PY_FROZEN
|
||||
path = None # imp compabilty
|
||||
suffix = mode = '' # imp compatibility
|
||||
elif (
|
||||
spec.origin == 'built-in'
|
||||
or static
|
||||
and issubclass(spec.loader, importlib.machinery.BuiltinImporter)
|
||||
):
|
||||
kind = C_BUILTIN
|
||||
path = None # imp compabilty
|
||||
suffix = mode = '' # imp compatibility
|
||||
elif spec.has_location:
|
||||
path = spec.origin
|
||||
suffix = os.path.splitext(path)[1]
|
||||
mode = 'r' if suffix in importlib.machinery.SOURCE_SUFFIXES else 'rb'
|
||||
|
||||
if suffix in importlib.machinery.SOURCE_SUFFIXES:
|
||||
kind = PY_SOURCE
|
||||
file = tokenize.open(path)
|
||||
elif suffix in importlib.machinery.BYTECODE_SUFFIXES:
|
||||
kind = PY_COMPILED
|
||||
file = open(path, 'rb')
|
||||
elif suffix in importlib.machinery.EXTENSION_SUFFIXES:
|
||||
kind = C_EXTENSION
|
||||
|
||||
else:
|
||||
path = None
|
||||
suffix = mode = ''
|
||||
|
||||
return file, path, (suffix, mode, kind)
|
||||
|
||||
|
||||
def get_frozen_object(module, paths=None):
|
||||
spec = find_spec(module, paths)
|
||||
if not spec:
|
||||
raise ImportError(f"Can't find {module}")
|
||||
return spec.loader.get_code(module)
|
||||
|
||||
|
||||
def get_module(module, paths, info):
|
||||
spec = find_spec(module, paths)
|
||||
if not spec:
|
||||
raise ImportError(f"Can't find {module}")
|
||||
return module_from_spec(spec)
|
||||
9
.venv/Lib/site-packages/setuptools/_importlib.py
Normal file
9
.venv/Lib/site-packages/setuptools/_importlib.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
import importlib_metadata as metadata # pragma: no cover
|
||||
else:
|
||||
import importlib.metadata as metadata # noqa: F401
|
||||
|
||||
|
||||
import importlib.resources as resources # noqa: F401
|
||||
23
.venv/Lib/site-packages/setuptools/_itertools.py
Normal file
23
.venv/Lib/site-packages/setuptools/_itertools.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from more_itertools import consume # noqa: F401
|
||||
|
||||
|
||||
# copied from jaraco.itertools 6.1
|
||||
def ensure_unique(iterable, key=lambda x: x):
|
||||
"""
|
||||
Wrap an iterable to raise a ValueError if non-unique values are encountered.
|
||||
|
||||
>>> list(ensure_unique('abc'))
|
||||
['a', 'b', 'c']
|
||||
>>> consume(ensure_unique('abca'))
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Duplicate element 'a' encountered.
|
||||
"""
|
||||
seen = set()
|
||||
seen_add = seen.add
|
||||
for element in iterable:
|
||||
k = key(element)
|
||||
if k in seen:
|
||||
raise ValueError(f"Duplicate element {element!r} encountered.")
|
||||
seen_add(k)
|
||||
yield element
|
||||
179
.venv/Lib/site-packages/setuptools/_normalization.py
Normal file
179
.venv/Lib/site-packages/setuptools/_normalization.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Helpers for normalization as expected in wheel/sdist/module file names
|
||||
and core metadata
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import packaging
|
||||
|
||||
# https://packaging.python.org/en/latest/specifications/core-metadata/#name
|
||||
_VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I)
|
||||
_UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9._-]+", re.I)
|
||||
_NON_ALPHANUMERIC = re.compile(r"[^A-Z0-9]+", re.I)
|
||||
_PEP440_FALLBACK = re.compile(r"^v?(?P<safe>(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I)
|
||||
|
||||
|
||||
def safe_identifier(name: str) -> str:
|
||||
"""Make a string safe to be used as Python identifier.
|
||||
>>> safe_identifier("12abc")
|
||||
'_12abc'
|
||||
>>> safe_identifier("__editable__.myns.pkg-78.9.3_local")
|
||||
'__editable___myns_pkg_78_9_3_local'
|
||||
"""
|
||||
safe = re.sub(r'\W|^(?=\d)', '_', name)
|
||||
assert safe.isidentifier()
|
||||
return safe
|
||||
|
||||
|
||||
def safe_name(component: str) -> str:
|
||||
"""Escape a component used as a project name according to Core Metadata.
|
||||
>>> safe_name("hello world")
|
||||
'hello-world'
|
||||
>>> safe_name("hello?world")
|
||||
'hello-world'
|
||||
>>> safe_name("hello_world")
|
||||
'hello_world'
|
||||
"""
|
||||
# See pkg_resources.safe_name
|
||||
return _UNSAFE_NAME_CHARS.sub("-", component)
|
||||
|
||||
|
||||
def safe_version(version: str) -> str:
|
||||
"""Convert an arbitrary string into a valid version string.
|
||||
Can still raise an ``InvalidVersion`` exception.
|
||||
To avoid exceptions use ``best_effort_version``.
|
||||
>>> safe_version("1988 12 25")
|
||||
'1988.12.25'
|
||||
>>> safe_version("v0.2.1")
|
||||
'0.2.1'
|
||||
>>> safe_version("v0.2?beta")
|
||||
'0.2b0'
|
||||
>>> safe_version("v0.2 beta")
|
||||
'0.2b0'
|
||||
>>> safe_version("ubuntu lts")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
packaging.version.InvalidVersion: Invalid version: 'ubuntu.lts'
|
||||
"""
|
||||
v = version.replace(' ', '.')
|
||||
try:
|
||||
return str(packaging.version.Version(v))
|
||||
except packaging.version.InvalidVersion:
|
||||
attempt = _UNSAFE_NAME_CHARS.sub("-", v)
|
||||
return str(packaging.version.Version(attempt))
|
||||
|
||||
|
||||
def best_effort_version(version: str) -> str:
|
||||
"""Convert an arbitrary string into a version-like string.
|
||||
Fallback when ``safe_version`` is not safe enough.
|
||||
>>> best_effort_version("v0.2 beta")
|
||||
'0.2b0'
|
||||
>>> best_effort_version("ubuntu lts")
|
||||
'0.dev0+sanitized.ubuntu.lts'
|
||||
>>> best_effort_version("0.23ubuntu1")
|
||||
'0.23.dev0+sanitized.ubuntu1'
|
||||
>>> best_effort_version("0.23-")
|
||||
'0.23.dev0+sanitized'
|
||||
>>> best_effort_version("0.-_")
|
||||
'0.dev0+sanitized'
|
||||
>>> best_effort_version("42.+?1")
|
||||
'42.dev0+sanitized.1'
|
||||
"""
|
||||
# See pkg_resources._forgiving_version
|
||||
try:
|
||||
return safe_version(version)
|
||||
except packaging.version.InvalidVersion:
|
||||
v = version.replace(' ', '.')
|
||||
match = _PEP440_FALLBACK.search(v)
|
||||
if match:
|
||||
safe = match["safe"]
|
||||
rest = v[len(safe) :]
|
||||
else:
|
||||
safe = "0"
|
||||
rest = version
|
||||
safe_rest = _NON_ALPHANUMERIC.sub(".", rest).strip(".")
|
||||
local = f"sanitized.{safe_rest}".strip(".")
|
||||
return safe_version(f"{safe}.dev0+{local}")
|
||||
|
||||
|
||||
def safe_extra(extra: str) -> str:
|
||||
"""Normalize extra name according to PEP 685
|
||||
>>> safe_extra("_FrIeNdLy-._.-bArD")
|
||||
'friendly-bard'
|
||||
>>> safe_extra("FrIeNdLy-._.-bArD__._-")
|
||||
'friendly-bard'
|
||||
"""
|
||||
return _NON_ALPHANUMERIC.sub("-", extra).strip("-").lower()
|
||||
|
||||
|
||||
def filename_component(value: str) -> str:
|
||||
"""Normalize each component of a filename (e.g. distribution/version part of wheel)
|
||||
Note: ``value`` needs to be already normalized.
|
||||
>>> filename_component("my-pkg")
|
||||
'my_pkg'
|
||||
"""
|
||||
return value.replace("-", "_").strip("_")
|
||||
|
||||
|
||||
def filename_component_broken(value: str) -> str:
|
||||
"""
|
||||
Produce the incorrect filename component for compatibility.
|
||||
|
||||
See pypa/setuptools#4167 for detailed analysis.
|
||||
|
||||
TODO: replace this with filename_component after pip 24 is
|
||||
nearly-ubiquitous.
|
||||
|
||||
>>> filename_component_broken('foo_bar-baz')
|
||||
'foo-bar-baz'
|
||||
"""
|
||||
return value.replace('_', '-')
|
||||
|
||||
|
||||
def safer_name(value: str) -> str:
|
||||
"""Like ``safe_name`` but can be used as filename component for wheel"""
|
||||
# See bdist_wheel.safer_name
|
||||
return (
|
||||
# Per https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization
|
||||
re.sub(r"[-_.]+", "-", safe_name(value))
|
||||
.lower()
|
||||
# Per https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode
|
||||
.replace("-", "_")
|
||||
)
|
||||
|
||||
|
||||
def safer_best_effort_version(value: str) -> str:
|
||||
"""Like ``best_effort_version`` but can be used as filename component for wheel"""
|
||||
# See bdist_wheel.safer_verion
|
||||
# TODO: Replace with only safe_version in the future (no need for best effort)
|
||||
return filename_component(best_effort_version(value))
|
||||
|
||||
|
||||
def _missing_canonicalize_license_expression(expression: str) -> str:
|
||||
"""
|
||||
Defer import error to affect only users that actually use it
|
||||
https://github.com/pypa/setuptools/issues/4894
|
||||
>>> _missing_canonicalize_license_expression("a OR b")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ImportError: ...Cannot import `packaging.licenses`...
|
||||
"""
|
||||
raise ImportError(
|
||||
"Cannot import `packaging.licenses`."
|
||||
"""
|
||||
Setuptools>=77.0.0 requires "packaging>=24.2" to work properly.
|
||||
Please make sure you have a suitable version installed.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
from packaging.licenses import (
|
||||
canonicalize_license_expression as _canonicalize_license_expression,
|
||||
)
|
||||
except ImportError: # pragma: nocover
|
||||
if not TYPE_CHECKING:
|
||||
# XXX: pyright is still upset even with # pyright: ignore[reportAssignmentType]
|
||||
_canonicalize_license_expression = _missing_canonicalize_license_expression
|
||||
84
.venv/Lib/site-packages/setuptools/_path.py
Normal file
84
.venv/Lib/site-packages/setuptools/_path.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, TypeVar, Union
|
||||
|
||||
from more_itertools import unique_everseen
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
StrPath: TypeAlias = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath
|
||||
StrPathT = TypeVar("StrPathT", bound=Union[str, os.PathLike[str]])
|
||||
|
||||
|
||||
def ensure_directory(path):
|
||||
"""Ensure that the parent directory of `path` exists"""
|
||||
dirname = os.path.dirname(path)
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
|
||||
|
||||
def same_path(p1: StrPath, p2: StrPath) -> bool:
|
||||
"""Differs from os.path.samefile because it does not require paths to exist.
|
||||
Purely string based (no comparison between i-nodes).
|
||||
>>> same_path("a/b", "./a/b")
|
||||
True
|
||||
>>> same_path("a/b", "a/./b")
|
||||
True
|
||||
>>> same_path("a/b", "././a/b")
|
||||
True
|
||||
>>> same_path("a/b", "./a/b/c/..")
|
||||
True
|
||||
>>> same_path("a/b", "../a/b/c")
|
||||
False
|
||||
>>> same_path("a", "a/b")
|
||||
False
|
||||
"""
|
||||
return normpath(p1) == normpath(p2)
|
||||
|
||||
|
||||
def normpath(filename: StrPath) -> str:
|
||||
"""Normalize a file/dir name for comparison purposes."""
|
||||
# See pkg_resources.normalize_path for notes about cygwin
|
||||
file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename
|
||||
return os.path.normcase(os.path.realpath(os.path.normpath(file)))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def paths_on_pythonpath(paths):
|
||||
"""
|
||||
Add the indicated paths to the head of the PYTHONPATH environment
|
||||
variable so that subprocesses will also see the packages at
|
||||
these paths.
|
||||
|
||||
Do this in a context that restores the value on exit.
|
||||
|
||||
>>> getfixture('monkeypatch').setenv('PYTHONPATH', 'anything')
|
||||
>>> with paths_on_pythonpath(['foo', 'bar']):
|
||||
... assert 'foo' in os.environ['PYTHONPATH']
|
||||
... assert 'anything' in os.environ['PYTHONPATH']
|
||||
>>> os.environ['PYTHONPATH']
|
||||
'anything'
|
||||
|
||||
>>> getfixture('monkeypatch').delenv('PYTHONPATH')
|
||||
>>> with paths_on_pythonpath(['foo', 'bar']):
|
||||
... assert 'foo' in os.environ['PYTHONPATH']
|
||||
>>> os.environ.get('PYTHONPATH')
|
||||
"""
|
||||
nothing = object()
|
||||
orig_pythonpath = os.environ.get('PYTHONPATH', nothing)
|
||||
current_pythonpath = os.environ.get('PYTHONPATH', '')
|
||||
try:
|
||||
prefix = os.pathsep.join(unique_everseen(paths))
|
||||
to_join = filter(None, [prefix, current_pythonpath])
|
||||
new_path = os.pathsep.join(to_join)
|
||||
if new_path:
|
||||
os.environ['PYTHONPATH'] = new_path
|
||||
yield
|
||||
finally:
|
||||
if orig_pythonpath is nothing:
|
||||
os.environ.pop('PYTHONPATH', None)
|
||||
else:
|
||||
os.environ['PYTHONPATH'] = orig_pythonpath
|
||||
42
.venv/Lib/site-packages/setuptools/_reqs.py
Normal file
42
.venv/Lib/site-packages/setuptools/_reqs.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Iterator
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING, Callable, TypeVar, Union, overload
|
||||
|
||||
import jaraco.text as text
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_StrOrIter: TypeAlias = Union[str, Iterable[str]]
|
||||
|
||||
|
||||
parse_req: Callable[[str], Requirement] = lru_cache()(Requirement)
|
||||
# Setuptools parses the same requirement many times
|
||||
# (e.g. first for validation than for normalisation),
|
||||
# so it might be worth to cache.
|
||||
|
||||
|
||||
def parse_strings(strs: _StrOrIter) -> Iterator[str]:
|
||||
"""
|
||||
Yield requirement strings for each specification in `strs`.
|
||||
|
||||
`strs` must be a string, or a (possibly-nested) iterable thereof.
|
||||
"""
|
||||
return text.join_continuation(map(text.drop_comment, text.yield_lines(strs)))
|
||||
|
||||
|
||||
# These overloads are only needed because of a mypy false-positive, pyright gets it right
|
||||
# https://github.com/python/mypy/issues/3737
|
||||
@overload
|
||||
def parse(strs: _StrOrIter) -> Iterator[Requirement]: ...
|
||||
@overload
|
||||
def parse(strs: _StrOrIter, parser: Callable[[str], _T]) -> Iterator[_T]: ...
|
||||
def parse(strs: _StrOrIter, parser: Callable[[str], _T] = parse_req) -> Iterator[_T]: # type: ignore[assignment]
|
||||
"""
|
||||
Replacement for ``pkg_resources.parse_requirements`` that uses ``packaging``.
|
||||
"""
|
||||
return map(parser, parse_strings(strs))
|
||||
53
.venv/Lib/site-packages/setuptools/_shutil.py
Normal file
53
.venv/Lib/site-packages/setuptools/_shutil.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Convenience layer on top of stdlib's shutil and os"""
|
||||
|
||||
import os
|
||||
import stat
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
from .compat import py311
|
||||
|
||||
from distutils import log
|
||||
|
||||
try:
|
||||
from os import chmod # pyright: ignore[reportAssignmentType]
|
||||
# Losing type-safety w/ pyright, but that's ok
|
||||
except ImportError: # pragma: no cover
|
||||
# Jython compatibility
|
||||
def chmod(*args: object, **kwargs: object) -> None: # type: ignore[misc] # Mypy reuses the imported definition anyway
|
||||
pass
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def attempt_chmod_verbose(path, mode):
|
||||
log.debug("changing mode of %s to %o", path, mode)
|
||||
try:
|
||||
chmod(path, mode)
|
||||
except OSError as e: # pragma: no cover
|
||||
log.debug("chmod failed: %s", e)
|
||||
|
||||
|
||||
# Must match shutil._OnExcCallback
|
||||
def _auto_chmod(
|
||||
func: Callable[..., _T], arg: str, exc: BaseException
|
||||
) -> _T: # pragma: no cover
|
||||
"""shutils onexc callback to automatically call chmod for certain functions."""
|
||||
# Only retry for scenarios known to have an issue
|
||||
if func in [os.unlink, os.remove] and os.name == 'nt':
|
||||
attempt_chmod_verbose(arg, stat.S_IWRITE)
|
||||
return func(arg)
|
||||
raise exc
|
||||
|
||||
|
||||
def rmtree(path, ignore_errors=False, onexc=_auto_chmod):
|
||||
"""
|
||||
Similar to ``shutil.rmtree`` but automatically executes ``chmod``
|
||||
for well know Windows failure scenarios.
|
||||
"""
|
||||
return py311.shutil_rmtree(path, ignore_errors, onexc)
|
||||
|
||||
|
||||
def rmdir(path, **opts):
|
||||
if os.path.isdir(path):
|
||||
rmtree(path, **opts)
|
||||
188
.venv/Lib/site-packages/setuptools/_static.py
Normal file
188
.venv/Lib/site-packages/setuptools/_static.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from functools import wraps
|
||||
from typing import TypeVar
|
||||
|
||||
import packaging.specifiers
|
||||
|
||||
from .warnings import SetuptoolsDeprecationWarning
|
||||
|
||||
|
||||
class Static:
|
||||
"""
|
||||
Wrapper for built-in object types that are allow setuptools to identify
|
||||
static core metadata (in opposition to ``Dynamic``, as defined :pep:`643`).
|
||||
|
||||
The trick is to mark values with :class:`Static` when they come from
|
||||
``pyproject.toml`` or ``setup.cfg``, so if any plugin overwrite the value
|
||||
with a built-in, setuptools will be able to recognise the change.
|
||||
|
||||
We inherit from built-in classes, so that we don't need to change the existing
|
||||
code base to deal with the new types.
|
||||
We also should strive for immutability objects to avoid changes after the
|
||||
initial parsing.
|
||||
"""
|
||||
|
||||
_mutated_: bool = False # TODO: Remove after deprecation warning is solved
|
||||
|
||||
|
||||
def _prevent_modification(target: type, method: str, copying: str) -> None:
|
||||
"""
|
||||
Because setuptools is very flexible we cannot fully prevent
|
||||
plugins and user customizations from modifying static values that were
|
||||
parsed from config files.
|
||||
But we can attempt to block "in-place" mutations and identify when they
|
||||
were done.
|
||||
"""
|
||||
fn = getattr(target, method, None)
|
||||
if fn is None:
|
||||
return
|
||||
|
||||
@wraps(fn)
|
||||
def _replacement(self: Static, *args, **kwargs):
|
||||
# TODO: After deprecation period raise NotImplementedError instead of warning
|
||||
# which obviated the existence and checks of the `_mutated_` attribute.
|
||||
self._mutated_ = True
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"Direct modification of value will be disallowed",
|
||||
f"""
|
||||
In an effort to implement PEP 643, direct/in-place changes of static values
|
||||
that come from configuration files are deprecated.
|
||||
If you need to modify this value, please first create a copy with {copying}
|
||||
and make sure conform to all relevant standards when overriding setuptools
|
||||
functionality (https://packaging.python.org/en/latest/specifications/).
|
||||
""",
|
||||
due_date=(2025, 10, 10), # Initially introduced in 2024-09-06
|
||||
)
|
||||
return fn(self, *args, **kwargs)
|
||||
|
||||
_replacement.__doc__ = "" # otherwise doctest may fail.
|
||||
setattr(target, method, _replacement)
|
||||
|
||||
|
||||
class Str(str, Static):
|
||||
pass
|
||||
|
||||
|
||||
class Tuple(tuple, Static):
|
||||
pass
|
||||
|
||||
|
||||
class List(list, Static):
|
||||
"""
|
||||
:meta private:
|
||||
>>> x = List([1, 2, 3])
|
||||
>>> is_static(x)
|
||||
True
|
||||
>>> x += [0] # doctest: +IGNORE_EXCEPTION_DETAIL
|
||||
Traceback (most recent call last):
|
||||
SetuptoolsDeprecationWarning: Direct modification ...
|
||||
>>> is_static(x) # no longer static after modification
|
||||
False
|
||||
>>> y = list(x)
|
||||
>>> y.clear()
|
||||
>>> y
|
||||
[]
|
||||
>>> y == x
|
||||
False
|
||||
>>> is_static(List(y))
|
||||
True
|
||||
"""
|
||||
|
||||
|
||||
# Make `List` immutable-ish
|
||||
# (certain places of setuptools/distutils issue a warn if we use tuple instead of list)
|
||||
for _method in (
|
||||
'__delitem__',
|
||||
'__iadd__',
|
||||
'__setitem__',
|
||||
'append',
|
||||
'clear',
|
||||
'extend',
|
||||
'insert',
|
||||
'remove',
|
||||
'reverse',
|
||||
'pop',
|
||||
):
|
||||
_prevent_modification(List, _method, "`list(value)`")
|
||||
|
||||
|
||||
class Dict(dict, Static):
|
||||
"""
|
||||
:meta private:
|
||||
>>> x = Dict({'a': 1, 'b': 2})
|
||||
>>> is_static(x)
|
||||
True
|
||||
>>> x['c'] = 0 # doctest: +IGNORE_EXCEPTION_DETAIL
|
||||
Traceback (most recent call last):
|
||||
SetuptoolsDeprecationWarning: Direct modification ...
|
||||
>>> x._mutated_
|
||||
True
|
||||
>>> is_static(x) # no longer static after modification
|
||||
False
|
||||
>>> y = dict(x)
|
||||
>>> y.popitem()
|
||||
('b', 2)
|
||||
>>> y == x
|
||||
False
|
||||
>>> is_static(Dict(y))
|
||||
True
|
||||
"""
|
||||
|
||||
|
||||
# Make `Dict` immutable-ish (we cannot inherit from types.MappingProxyType):
|
||||
for _method in (
|
||||
'__delitem__',
|
||||
'__ior__',
|
||||
'__setitem__',
|
||||
'clear',
|
||||
'pop',
|
||||
'popitem',
|
||||
'setdefault',
|
||||
'update',
|
||||
):
|
||||
_prevent_modification(Dict, _method, "`dict(value)`")
|
||||
|
||||
|
||||
class SpecifierSet(packaging.specifiers.SpecifierSet, Static):
|
||||
"""Not exactly a built-in type but useful for ``requires-python``"""
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def noop(value: T) -> T:
|
||||
"""
|
||||
>>> noop(42)
|
||||
42
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
_CONVERSIONS = {str: Str, tuple: Tuple, list: List, dict: Dict}
|
||||
|
||||
|
||||
def attempt_conversion(value: T) -> T:
|
||||
"""
|
||||
>>> is_static(attempt_conversion("hello"))
|
||||
True
|
||||
>>> is_static(object())
|
||||
False
|
||||
"""
|
||||
return _CONVERSIONS.get(type(value), noop)(value) # type: ignore[call-overload]
|
||||
|
||||
|
||||
def is_static(value: object) -> bool:
|
||||
"""
|
||||
>>> is_static(a := Dict({'a': 1}))
|
||||
True
|
||||
>>> is_static(dict(a))
|
||||
False
|
||||
>>> is_static(b := List([1, 2, 3]))
|
||||
True
|
||||
>>> is_static(list(b))
|
||||
False
|
||||
"""
|
||||
return isinstance(value, Static) and not value._mutated_
|
||||
|
||||
|
||||
EMPTY_LIST = List()
|
||||
EMPTY_DICT = Dict()
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,166 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: autocommand
|
||||
Version: 2.2.2
|
||||
Summary: A library to create a command-line program from a function
|
||||
Home-page: https://github.com/Lucretiel/autocommand
|
||||
Author: Nathan West
|
||||
License: LGPLv3
|
||||
Project-URL: Homepage, https://github.com/Lucretiel/autocommand
|
||||
Project-URL: Bug Tracker, https://github.com/Lucretiel/autocommand/issues
|
||||
Platform: any
|
||||
Classifier: Development Status :: 6 - Mature
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Topic :: Software Development
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
|
||||
[](https://badge.fury.io/py/autocommand)
|
||||
|
||||
# autocommand
|
||||
|
||||
A library to automatically generate and run simple argparse parsers from function signatures.
|
||||
|
||||
## Installation
|
||||
|
||||
Autocommand is installed via pip:
|
||||
|
||||
```
|
||||
$ pip install autocommand
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Autocommand turns a function into a command-line program. It converts the function's parameter signature into command-line arguments, and automatically runs the function if the module was called as `__main__`. In effect, it lets your create a smart main function.
|
||||
|
||||
```python
|
||||
from autocommand import autocommand
|
||||
|
||||
# This program takes exactly one argument and echos it.
|
||||
@autocommand(__name__)
|
||||
def echo(thing):
|
||||
print(thing)
|
||||
```
|
||||
|
||||
```
|
||||
$ python echo.py hello
|
||||
hello
|
||||
$ python echo.py -h
|
||||
usage: echo [-h] thing
|
||||
|
||||
positional arguments:
|
||||
thing
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
$ python echo.py hello world # too many arguments
|
||||
usage: echo.py [-h] thing
|
||||
echo.py: error: unrecognized arguments: world
|
||||
```
|
||||
|
||||
As you can see, autocommand converts the signature of the function into an argument spec. When you run the file as a program, autocommand collects the command-line arguments and turns them into function arguments. The function is executed with these arguments, and then the program exits with the return value of the function, via `sys.exit`. Autocommand also automatically creates a usage message, which can be invoked with `-h` or `--help`, and automatically prints an error message when provided with invalid arguments.
|
||||
|
||||
### Types
|
||||
|
||||
You can use a type annotation to give an argument a type. Any type (or in fact any callable) that returns an object when given a string argument can be used, though there are a few special cases that are described later.
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def net_client(host, port: int):
|
||||
...
|
||||
```
|
||||
|
||||
Autocommand will catch `TypeErrors` raised by the type during argument parsing, so you can supply a callable and do some basic argument validation as well.
|
||||
|
||||
### Trailing Arguments
|
||||
|
||||
You can add a `*args` parameter to your function to give it trailing arguments. The command will collect 0 or more trailing arguments and supply them to `args` as a tuple. If a type annotation is supplied, the type is applied to each argument.
|
||||
|
||||
```python
|
||||
# Write the contents of each file, one by one
|
||||
@autocommand(__name__)
|
||||
def cat(*files):
|
||||
for filename in files:
|
||||
with open(filename) as file:
|
||||
for line in file:
|
||||
print(line.rstrip())
|
||||
```
|
||||
|
||||
```
|
||||
$ python cat.py -h
|
||||
usage: ipython [-h] [file [file ...]]
|
||||
|
||||
positional arguments:
|
||||
file
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
To create `--option` switches, just assign a default. Autocommand will automatically create `--long` and `-s`hort switches.
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def do_with_config(argument, config='~/foo.conf'):
|
||||
pass
|
||||
```
|
||||
|
||||
```
|
||||
$ python example.py -h
|
||||
usage: example.py [-h] [-c CONFIG] argument
|
||||
|
||||
positional arguments:
|
||||
argument
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG, --config CONFIG
|
||||
```
|
||||
|
||||
The option's type is automatically deduced from the default, unless one is explicitly given in an annotation:
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def http_connect(host, port=80):
|
||||
print('{}:{}'.format(host, port))
|
||||
```
|
||||
|
||||
```
|
||||
$ python http.py -h
|
||||
usage: http.py [-h] [-p PORT] host
|
||||
|
||||
positional arguments:
|
||||
host
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-p PORT, --port PORT
|
||||
$ python http.py localhost
|
||||
localhost:80
|
||||
$ python http.py localhost -p 8080
|
||||
localhost:8080
|
||||
$ python http.py localhost -p blah
|
||||
usage: http.py [-h] [-p PORT] host
|
||||
http.py: error: argument -p/--port: invalid int value: 'blah'
|
||||
```
|
||||
|
||||
#### None
|
||||
|
||||
If an option is given a default value of `None`, it reads in a value as normal, but supplies `None` if the option isn't provided.
|
||||
|
||||
#### Switches
|
||||
|
||||
If an argument is given a default value of `True` or `False`, or
|
||||
given an explicit `bool` type, it becomes an option switch.
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def example(verbose=False, quiet=False):
|
||||
pass
|
||||
```
|
||||
|
||||
```
|
||||
$ python example.py -h
|
||||
usage: example.py [-h] [-v] [-q]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
```
|
||||
|
||||
Autocommand attempts to do the "correct thing" in these cases- if the default is `True`, then supplying the switch makes the argument `False`; if the type is `bool` and the default is some other `True` value, then supplying the switch makes the argument `False`, while not supplying the switch makes the argument the default value.
|
||||
|
||||
Autocommand also supports the creation of switch inverters. Pass `add_nos=True` to `autocommand` to enable this.
|
||||
|
||||
```
|
||||
@autocommand(__name__, add_nos=True)
|
||||
def example(verbose=False):
|
||||
pass
|
||||
```
|
||||
|
||||
```
|
||||
$ python example.py -h
|
||||
usage: ipython [-h] [-v] [--no-verbose]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose
|
||||
--no-verbose
|
||||
```
|
||||
|
||||
Using the `--no-` version of a switch will pass the opposite value in as a function argument. If multiple switches are present, the last one takes precedence.
|
||||
|
||||
#### Files
|
||||
|
||||
If the default value is a file object, such as `sys.stdout`, then autocommand just looks for a string, for a file path. It doesn't do any special checking on the string, though (such as checking if the file exists); it's better to let the client decide how to handle errors in this case. Instead, it provides a special context manager called `smart_open`, which behaves exactly like `open` if a filename or other openable type is provided, but also lets you use already open files:
|
||||
|
||||
```python
|
||||
from autocommand import autocommand, smart_open
|
||||
import sys
|
||||
|
||||
# Write the contents of stdin, or a file, to stdout
|
||||
@autocommand(__name__)
|
||||
def write_out(infile=sys.stdin):
|
||||
with smart_open(infile) as f:
|
||||
for line in f:
|
||||
print(line.rstrip())
|
||||
# If a file was opened, it is closed here. If it was just stdin, it is untouched.
|
||||
```
|
||||
|
||||
```
|
||||
$ echo "Hello World!" | python write_out.py | tee hello.txt
|
||||
Hello World!
|
||||
$ python write_out.py --infile hello.txt
|
||||
Hello World!
|
||||
```
|
||||
|
||||
### Descriptions and docstrings
|
||||
|
||||
The `autocommand` decorator accepts `description` and `epilog` kwargs, corresponding to the `description <https://docs.python.org/3/library/argparse.html#description>`_ and `epilog <https://docs.python.org/3/library/argparse.html#epilog>`_ of the `ArgumentParser`. If no description is given, but the decorated function has a docstring, then it is taken as the `description` for the `ArgumentParser`. You can also provide both the description and epilog in the docstring by splitting it into two sections with 4 or more - characters.
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def copy(infile=sys.stdin, outfile=sys.stdout):
|
||||
'''
|
||||
Copy an the contents of a file (or stdin) to another file (or stdout)
|
||||
----------
|
||||
Some extra documentation in the epilog
|
||||
'''
|
||||
with smart_open(infile) as istr:
|
||||
with smart_open(outfile, 'w') as ostr:
|
||||
for line in istr:
|
||||
ostr.write(line)
|
||||
```
|
||||
|
||||
```
|
||||
$ python copy.py -h
|
||||
usage: copy.py [-h] [-i INFILE] [-o OUTFILE]
|
||||
|
||||
Copy an the contents of a file (or stdin) to another file (or stdout)
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i INFILE, --infile INFILE
|
||||
-o OUTFILE, --outfile OUTFILE
|
||||
|
||||
Some extra documentation in the epilog
|
||||
$ echo "Hello World" | python copy.py --outfile hello.txt
|
||||
$ python copy.py --infile hello.txt --outfile hello2.txt
|
||||
$ python copy.py --infile hello2.txt
|
||||
Hello World
|
||||
```
|
||||
|
||||
### Parameter descriptions
|
||||
|
||||
You can also attach description text to individual parameters in the annotation. To attach both a type and a description, supply them both in any order in a tuple
|
||||
|
||||
```python
|
||||
@autocommand(__name__)
|
||||
def copy_net(
|
||||
infile: 'The name of the file to send',
|
||||
host: 'The host to send the file to',
|
||||
port: (int, 'The port to connect to')):
|
||||
|
||||
'''
|
||||
Copy a file over raw TCP to a remote destination.
|
||||
'''
|
||||
# Left as an exercise to the reader
|
||||
```
|
||||
|
||||
### Decorators and wrappers
|
||||
|
||||
Autocommand automatically follows wrapper chains created by `@functools.wraps`. This means that you can apply other wrapping decorators to your main function, and autocommand will still correctly detect the signature.
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from autocommand import autocommand
|
||||
|
||||
def print_yielded(func):
|
||||
'''
|
||||
Convert a generator into a function that prints all yielded elements
|
||||
'''
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
for thing in func(*args, **kwargs):
|
||||
print(thing)
|
||||
return wrapper
|
||||
|
||||
@autocommand(__name__,
|
||||
description= 'Print all the values from START to STOP, inclusive, in steps of STEP',
|
||||
epilog= 'STOP and STEP default to 1')
|
||||
@print_yielded
|
||||
def seq(stop, start=1, step=1):
|
||||
for i in range(start, stop + 1, step):
|
||||
yield i
|
||||
```
|
||||
|
||||
```
|
||||
$ seq.py -h
|
||||
usage: seq.py [-h] [-s START] [-S STEP] stop
|
||||
|
||||
Print all the values from START to STOP, inclusive, in steps of STEP
|
||||
|
||||
positional arguments:
|
||||
stop
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-s START, --start START
|
||||
-S STEP, --step STEP
|
||||
|
||||
STOP and STEP default to 1
|
||||
```
|
||||
|
||||
Even though autocommand is being applied to the `wrapper` returned by `print_yielded`, it still retreives the signature of the underlying `seq` function to create the argument parsing.
|
||||
|
||||
### Custom Parser
|
||||
|
||||
While autocommand's automatic parser generator is a powerful convenience, it doesn't cover all of the different features that argparse provides. If you need these features, you can provide your own parser as a kwarg to `autocommand`:
|
||||
|
||||
```python
|
||||
from argparse import ArgumentParser
|
||||
from autocommand import autocommand
|
||||
|
||||
parser = ArgumentParser()
|
||||
# autocommand can't do optional positonal parameters
|
||||
parser.add_argument('arg', nargs='?')
|
||||
# or mutually exclusive options
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('-v', '--verbose', action='store_true')
|
||||
group.add_argument('-q', '--quiet', action='store_true')
|
||||
|
||||
@autocommand(__name__, parser=parser)
|
||||
def main(arg, verbose, quiet):
|
||||
print(arg, verbose, quiet)
|
||||
```
|
||||
|
||||
```
|
||||
$ python parser.py -h
|
||||
usage: write_file.py [-h] [-v | -q] [arg]
|
||||
|
||||
positional arguments:
|
||||
arg
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
$ python parser.py
|
||||
None False False
|
||||
$ python parser.py hello
|
||||
hello False False
|
||||
$ python parser.py -v
|
||||
None True False
|
||||
$ python parser.py -q
|
||||
None False True
|
||||
$ python parser.py -vq
|
||||
usage: parser.py [-h] [-v | -q] [arg]
|
||||
parser.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
|
||||
```
|
||||
|
||||
Any parser should work fine, so long as each of the parser's arguments has a corresponding parameter in the decorated main function. The order of parameters doesn't matter, as long as they are all present. Note that when using a custom parser, autocommand doesn't modify the parser or the retrieved arguments. This means that no description/epilog will be added, and the function's type annotations and defaults (if present) will be ignored.
|
||||
|
||||
## Testing and Library use
|
||||
|
||||
The decorated function is only called and exited from if the first argument to `autocommand` is `'__main__'` or `True`. If it is neither of these values, or no argument is given, then a new main function is created by the decorator. This function has the signature `main(argv=None)`, and is intended to be called with arguments as if via `main(sys.argv[1:])`. The function has the attributes `parser` and `main`, which are the generated `ArgumentParser` and the original main function that was decorated. This is to facilitate testing and library use of your main. Calling the function triggers a `parse_args()` with the supplied arguments, and returns the result of the main function. Note that, while it returns instead of calling `sys.exit`, the `parse_args()` function will raise a `SystemExit` in the event of a parsing error or `-h/--help` argument.
|
||||
|
||||
```python
|
||||
@autocommand()
|
||||
def test_prog(arg1, arg2: int, quiet=False, verbose=False):
|
||||
if not quiet:
|
||||
print(arg1, arg2)
|
||||
if verbose:
|
||||
print("LOUD NOISES")
|
||||
|
||||
return 0
|
||||
|
||||
print(test_prog(['-v', 'hello', '80']))
|
||||
```
|
||||
|
||||
```
|
||||
$ python test_prog.py
|
||||
hello 80
|
||||
LOUD NOISES
|
||||
0
|
||||
```
|
||||
|
||||
If the function is called with no arguments, `sys.argv[1:]` is used. This is to allow the autocommand function to be used as a setuptools entry point.
|
||||
|
||||
## Exceptions and limitations
|
||||
|
||||
- There are a few possible exceptions that `autocommand` can raise. All of them derive from `autocommand.AutocommandError`.
|
||||
|
||||
- If an invalid annotation is given (that is, it isn't a `type`, `str`, `(type, str)`, or `(str, type)`, an `AnnotationError` is raised. The `type` may be any callable, as described in the `Types`_ section.
|
||||
- If the function has a `**kwargs` parameter, a `KWargError` is raised.
|
||||
- If, somehow, the function has a positional-only parameter, a `PositionalArgError` is raised. This means that the argument doesn't have a name, which is currently not possible with a plain `def` or `lambda`, though many built-in functions have this kind of parameter.
|
||||
|
||||
- There are a few argparse features that are not supported by autocommand.
|
||||
|
||||
- It isn't possible to have an optional positional argument (as opposed to a `--option`). POSIX thinks this is bad form anyway.
|
||||
- It isn't possible to have mutually exclusive arguments or options
|
||||
- It isn't possible to have subcommands or subparsers, though I'm working on a few solutions involving classes or nested function definitions to allow this.
|
||||
|
||||
## Development
|
||||
|
||||
Autocommand cannot be important from the project root; this is to enforce separation of concerns and prevent accidental importing of `setup.py` or tests. To develop, install the project in editable mode:
|
||||
|
||||
```
|
||||
$ python setup.py develop
|
||||
```
|
||||
|
||||
This will create a link to the source files in the deployment directory, so that any source changes are reflected when it is imported.
|
||||
@@ -0,0 +1,18 @@
|
||||
autocommand-2.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
autocommand-2.2.2.dist-info/LICENSE,sha256=reeNBJgtaZctREqOFKlPh6IzTdOFXMgDSOqOJAqg3y0,7634
|
||||
autocommand-2.2.2.dist-info/METADATA,sha256=OADZuR3O6iBlpu1ieTgzYul6w4uOVrk0P0BO5TGGAJk,15006
|
||||
autocommand-2.2.2.dist-info/RECORD,,
|
||||
autocommand-2.2.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
||||
autocommand-2.2.2.dist-info/top_level.txt,sha256=AzfhgKKS8EdAwWUTSF8mgeVQbXOY9kokHB6kSqwwqu0,12
|
||||
autocommand/__init__.py,sha256=zko5Rnvolvb-UXjCx_2ArPTGBWwUK5QY4LIQIKYR7As,1037
|
||||
autocommand/__pycache__/__init__.cpython-312.pyc,,
|
||||
autocommand/__pycache__/autoasync.cpython-312.pyc,,
|
||||
autocommand/__pycache__/autocommand.cpython-312.pyc,,
|
||||
autocommand/__pycache__/automain.cpython-312.pyc,,
|
||||
autocommand/__pycache__/autoparse.cpython-312.pyc,,
|
||||
autocommand/__pycache__/errors.cpython-312.pyc,,
|
||||
autocommand/autoasync.py,sha256=AMdyrxNS4pqWJfP_xuoOcImOHWD-qT7x06wmKN1Vp-U,5680
|
||||
autocommand/autocommand.py,sha256=hmkEmQ72HtL55gnURVjDOnsfYlGd5lLXbvT4KG496Qw,2505
|
||||
autocommand/automain.py,sha256=A2b8i754Mxc_DjU9WFr6vqYDWlhz0cn8miu8d8EsxV8,2076
|
||||
autocommand/autoparse.py,sha256=WVWmZJPcbzUKXP40raQw_0HD8qPJ2V9VG1eFFmmnFxw,11642
|
||||
autocommand/errors.py,sha256=7aa3roh9Herd6nIKpQHNWEslWE8oq7GiHYVUuRqORnA,886
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.38.4)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
autocommand
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright 2014-2016 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# flake8 flags all these imports as unused, hence the NOQAs everywhere.
|
||||
|
||||
from .automain import automain # NOQA
|
||||
from .autoparse import autoparse, smart_open # NOQA
|
||||
from .autocommand import autocommand # NOQA
|
||||
|
||||
try:
|
||||
from .autoasync import autoasync # NOQA
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
@@ -0,0 +1,142 @@
|
||||
# Copyright 2014-2015 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from asyncio import get_event_loop, iscoroutine
|
||||
from functools import wraps
|
||||
from inspect import signature
|
||||
|
||||
|
||||
async def _run_forever_coro(coro, args, kwargs, loop):
|
||||
'''
|
||||
This helper function launches an async main function that was tagged with
|
||||
forever=True. There are two possibilities:
|
||||
|
||||
- The function is a normal function, which handles initializing the event
|
||||
loop, which is then run forever
|
||||
- The function is a coroutine, which needs to be scheduled in the event
|
||||
loop, which is then run forever
|
||||
- There is also the possibility that the function is a normal function
|
||||
wrapping a coroutine function
|
||||
|
||||
The function is therefore called unconditionally and scheduled in the event
|
||||
loop if the return value is a coroutine object.
|
||||
|
||||
The reason this is a separate function is to make absolutely sure that all
|
||||
the objects created are garbage collected after all is said and done; we
|
||||
do this to ensure that any exceptions raised in the tasks are collected
|
||||
ASAP.
|
||||
'''
|
||||
|
||||
# Personal note: I consider this an antipattern, as it relies on the use of
|
||||
# unowned resources. The setup function dumps some stuff into the event
|
||||
# loop where it just whirls in the ether without a well defined owner or
|
||||
# lifetime. For this reason, there's a good chance I'll remove the
|
||||
# forever=True feature from autoasync at some point in the future.
|
||||
thing = coro(*args, **kwargs)
|
||||
if iscoroutine(thing):
|
||||
await thing
|
||||
|
||||
|
||||
def autoasync(coro=None, *, loop=None, forever=False, pass_loop=False):
|
||||
'''
|
||||
Convert an asyncio coroutine into a function which, when called, is
|
||||
evaluted in an event loop, and the return value returned. This is intented
|
||||
to make it easy to write entry points into asyncio coroutines, which
|
||||
otherwise need to be explictly evaluted with an event loop's
|
||||
run_until_complete.
|
||||
|
||||
If `loop` is given, it is used as the event loop to run the coro in. If it
|
||||
is None (the default), the loop is retreived using asyncio.get_event_loop.
|
||||
This call is defered until the decorated function is called, so that
|
||||
callers can install custom event loops or event loop policies after
|
||||
@autoasync is applied.
|
||||
|
||||
If `forever` is True, the loop is run forever after the decorated coroutine
|
||||
is finished. Use this for servers created with asyncio.start_server and the
|
||||
like.
|
||||
|
||||
If `pass_loop` is True, the event loop object is passed into the coroutine
|
||||
as the `loop` kwarg when the wrapper function is called. In this case, the
|
||||
wrapper function's __signature__ is updated to remove this parameter, so
|
||||
that autoparse can still be used on it without generating a parameter for
|
||||
`loop`.
|
||||
|
||||
This coroutine can be called with ( @autoasync(...) ) or without
|
||||
( @autoasync ) arguments.
|
||||
|
||||
Examples:
|
||||
|
||||
@autoasync
|
||||
def get_file(host, port):
|
||||
reader, writer = yield from asyncio.open_connection(host, port)
|
||||
data = reader.read()
|
||||
sys.stdout.write(data.decode())
|
||||
|
||||
get_file(host, port)
|
||||
|
||||
@autoasync(forever=True, pass_loop=True)
|
||||
def server(host, port, loop):
|
||||
yield_from loop.create_server(Proto, host, port)
|
||||
|
||||
server('localhost', 8899)
|
||||
|
||||
'''
|
||||
if coro is None:
|
||||
return lambda c: autoasync(
|
||||
c, loop=loop,
|
||||
forever=forever,
|
||||
pass_loop=pass_loop)
|
||||
|
||||
# The old and new signatures are required to correctly bind the loop
|
||||
# parameter in 100% of cases, even if it's a positional parameter.
|
||||
# NOTE: A future release will probably require the loop parameter to be
|
||||
# a kwonly parameter.
|
||||
if pass_loop:
|
||||
old_sig = signature(coro)
|
||||
new_sig = old_sig.replace(parameters=(
|
||||
param for name, param in old_sig.parameters.items()
|
||||
if name != "loop"))
|
||||
|
||||
@wraps(coro)
|
||||
def autoasync_wrapper(*args, **kwargs):
|
||||
# Defer the call to get_event_loop so that, if a custom policy is
|
||||
# installed after the autoasync decorator, it is respected at call time
|
||||
local_loop = get_event_loop() if loop is None else loop
|
||||
|
||||
# Inject the 'loop' argument. We have to use this signature binding to
|
||||
# ensure it's injected in the correct place (positional, keyword, etc)
|
||||
if pass_loop:
|
||||
bound_args = old_sig.bind_partial()
|
||||
bound_args.arguments.update(
|
||||
loop=local_loop,
|
||||
**new_sig.bind(*args, **kwargs).arguments)
|
||||
args, kwargs = bound_args.args, bound_args.kwargs
|
||||
|
||||
if forever:
|
||||
local_loop.create_task(_run_forever_coro(
|
||||
coro, args, kwargs, local_loop
|
||||
))
|
||||
local_loop.run_forever()
|
||||
else:
|
||||
return local_loop.run_until_complete(coro(*args, **kwargs))
|
||||
|
||||
# Attach the updated signature. This allows 'pass_loop' to be used with
|
||||
# autoparse
|
||||
if pass_loop:
|
||||
autoasync_wrapper.__signature__ = new_sig
|
||||
|
||||
return autoasync_wrapper
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright 2014-2015 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .autoparse import autoparse
|
||||
from .automain import automain
|
||||
try:
|
||||
from .autoasync import autoasync
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
def autocommand(
|
||||
module, *,
|
||||
description=None,
|
||||
epilog=None,
|
||||
add_nos=False,
|
||||
parser=None,
|
||||
loop=None,
|
||||
forever=False,
|
||||
pass_loop=False):
|
||||
|
||||
if callable(module):
|
||||
raise TypeError('autocommand requires a module name argument')
|
||||
|
||||
def autocommand_decorator(func):
|
||||
# Step 1: if requested, run it all in an asyncio event loop. autoasync
|
||||
# patches the __signature__ of the decorated function, so that in the
|
||||
# event that pass_loop is True, the `loop` parameter of the original
|
||||
# function will *not* be interpreted as a command-line argument by
|
||||
# autoparse
|
||||
if loop is not None or forever or pass_loop:
|
||||
func = autoasync(
|
||||
func,
|
||||
loop=None if loop is True else loop,
|
||||
pass_loop=pass_loop,
|
||||
forever=forever)
|
||||
|
||||
# Step 2: create parser. We do this second so that the arguments are
|
||||
# parsed and passed *before* entering the asyncio event loop, if it
|
||||
# exists. This simplifies the stack trace and ensures errors are
|
||||
# reported earlier. It also ensures that errors raised during parsing &
|
||||
# passing are still raised if `forever` is True.
|
||||
func = autoparse(
|
||||
func,
|
||||
description=description,
|
||||
epilog=epilog,
|
||||
add_nos=add_nos,
|
||||
parser=parser)
|
||||
|
||||
# Step 3: call the function automatically if __name__ == '__main__' (or
|
||||
# if True was provided)
|
||||
func = automain(module)(func)
|
||||
|
||||
return func
|
||||
|
||||
return autocommand_decorator
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright 2014-2015 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from .errors import AutocommandError
|
||||
|
||||
|
||||
class AutomainRequiresModuleError(AutocommandError, TypeError):
|
||||
pass
|
||||
|
||||
|
||||
def automain(module, *, args=(), kwargs=None):
|
||||
'''
|
||||
This decorator automatically invokes a function if the module is being run
|
||||
as the "__main__" module. Optionally, provide args or kwargs with which to
|
||||
call the function. If `module` is "__main__", the function is called, and
|
||||
the program is `sys.exit`ed with the return value. You can also pass `True`
|
||||
to cause the function to be called unconditionally. If the function is not
|
||||
called, it is returned unchanged by the decorator.
|
||||
|
||||
Usage:
|
||||
|
||||
@automain(__name__) # Pass __name__ to check __name__=="__main__"
|
||||
def main():
|
||||
...
|
||||
|
||||
If __name__ is "__main__" here, the main function is called, and then
|
||||
sys.exit called with the return value.
|
||||
'''
|
||||
|
||||
# Check that @automain(...) was called, rather than @automain
|
||||
if callable(module):
|
||||
raise AutomainRequiresModuleError(module)
|
||||
|
||||
if module == '__main__' or module is True:
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
# Use a function definition instead of a lambda for a neater traceback
|
||||
def automain_decorator(main):
|
||||
sys.exit(main(*args, **kwargs))
|
||||
|
||||
return automain_decorator
|
||||
else:
|
||||
return lambda main: main
|
||||
@@ -0,0 +1,333 @@
|
||||
# Copyright 2014-2015 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from re import compile as compile_regex
|
||||
from inspect import signature, getdoc, Parameter
|
||||
from argparse import ArgumentParser
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from io import IOBase
|
||||
from autocommand.errors import AutocommandError
|
||||
|
||||
|
||||
_empty = Parameter.empty
|
||||
|
||||
|
||||
class AnnotationError(AutocommandError):
|
||||
'''Annotation error: annotation must be a string, type, or tuple of both'''
|
||||
|
||||
|
||||
class PositionalArgError(AutocommandError):
|
||||
'''
|
||||
Postional Arg Error: autocommand can't handle postional-only parameters
|
||||
'''
|
||||
|
||||
|
||||
class KWArgError(AutocommandError):
|
||||
'''kwarg Error: autocommand can't handle a **kwargs parameter'''
|
||||
|
||||
|
||||
class DocstringError(AutocommandError):
|
||||
'''Docstring error'''
|
||||
|
||||
|
||||
class TooManySplitsError(DocstringError):
|
||||
'''
|
||||
The docstring had too many ---- section splits. Currently we only support
|
||||
using up to a single split, to split the docstring into description and
|
||||
epilog parts.
|
||||
'''
|
||||
|
||||
|
||||
def _get_type_description(annotation):
|
||||
'''
|
||||
Given an annotation, return the (type, description) for the parameter.
|
||||
If you provide an annotation that is somehow both a string and a callable,
|
||||
the behavior is undefined.
|
||||
'''
|
||||
if annotation is _empty:
|
||||
return None, None
|
||||
elif callable(annotation):
|
||||
return annotation, None
|
||||
elif isinstance(annotation, str):
|
||||
return None, annotation
|
||||
elif isinstance(annotation, tuple):
|
||||
try:
|
||||
arg1, arg2 = annotation
|
||||
except ValueError as e:
|
||||
raise AnnotationError(annotation) from e
|
||||
else:
|
||||
if callable(arg1) and isinstance(arg2, str):
|
||||
return arg1, arg2
|
||||
elif isinstance(arg1, str) and callable(arg2):
|
||||
return arg2, arg1
|
||||
|
||||
raise AnnotationError(annotation)
|
||||
|
||||
|
||||
def _add_arguments(param, parser, used_char_args, add_nos):
|
||||
'''
|
||||
Add the argument(s) to an ArgumentParser (using add_argument) for a given
|
||||
parameter. used_char_args is the set of -short options currently already in
|
||||
use, and is updated (if necessary) by this function. If add_nos is True,
|
||||
this will also add an inverse switch for all boolean options. For
|
||||
instance, for the boolean parameter "verbose", this will create --verbose
|
||||
and --no-verbose.
|
||||
'''
|
||||
|
||||
# Impl note: This function is kept separate from make_parser because it's
|
||||
# already very long and I wanted to separate out as much as possible into
|
||||
# its own call scope, to prevent even the possibility of suble mutation
|
||||
# bugs.
|
||||
if param.kind is param.POSITIONAL_ONLY:
|
||||
raise PositionalArgError(param)
|
||||
elif param.kind is param.VAR_KEYWORD:
|
||||
raise KWArgError(param)
|
||||
|
||||
# These are the kwargs for the add_argument function.
|
||||
arg_spec = {}
|
||||
is_option = False
|
||||
|
||||
# Get the type and default from the annotation.
|
||||
arg_type, description = _get_type_description(param.annotation)
|
||||
|
||||
# Get the default value
|
||||
default = param.default
|
||||
|
||||
# If there is no explicit type, and the default is present and not None,
|
||||
# infer the type from the default.
|
||||
if arg_type is None and default not in {_empty, None}:
|
||||
arg_type = type(default)
|
||||
|
||||
# Add default. The presence of a default means this is an option, not an
|
||||
# argument.
|
||||
if default is not _empty:
|
||||
arg_spec['default'] = default
|
||||
is_option = True
|
||||
|
||||
# Add the type
|
||||
if arg_type is not None:
|
||||
# Special case for bool: make it just a --switch
|
||||
if arg_type is bool:
|
||||
if not default or default is _empty:
|
||||
arg_spec['action'] = 'store_true'
|
||||
else:
|
||||
arg_spec['action'] = 'store_false'
|
||||
|
||||
# Switches are always options
|
||||
is_option = True
|
||||
|
||||
# Special case for file types: make it a string type, for filename
|
||||
elif isinstance(default, IOBase):
|
||||
arg_spec['type'] = str
|
||||
|
||||
# TODO: special case for list type.
|
||||
# - How to specificy type of list members?
|
||||
# - param: [int]
|
||||
# - param: int =[]
|
||||
# - action='append' vs nargs='*'
|
||||
|
||||
else:
|
||||
arg_spec['type'] = arg_type
|
||||
|
||||
# nargs: if the signature includes *args, collect them as trailing CLI
|
||||
# arguments in a list. *args can't have a default value, so it can never be
|
||||
# an option.
|
||||
if param.kind is param.VAR_POSITIONAL:
|
||||
# TODO: consider depluralizing metavar/name here.
|
||||
arg_spec['nargs'] = '*'
|
||||
|
||||
# Add description.
|
||||
if description is not None:
|
||||
arg_spec['help'] = description
|
||||
|
||||
# Get the --flags
|
||||
flags = []
|
||||
name = param.name
|
||||
|
||||
if is_option:
|
||||
# Add the first letter as a -short option.
|
||||
for letter in name[0], name[0].swapcase():
|
||||
if letter not in used_char_args:
|
||||
used_char_args.add(letter)
|
||||
flags.append('-{}'.format(letter))
|
||||
break
|
||||
|
||||
# If the parameter is a --long option, or is a -short option that
|
||||
# somehow failed to get a flag, add it.
|
||||
if len(name) > 1 or not flags:
|
||||
flags.append('--{}'.format(name))
|
||||
|
||||
arg_spec['dest'] = name
|
||||
else:
|
||||
flags.append(name)
|
||||
|
||||
parser.add_argument(*flags, **arg_spec)
|
||||
|
||||
# Create the --no- version for boolean switches
|
||||
if add_nos and arg_type is bool:
|
||||
parser.add_argument(
|
||||
'--no-{}'.format(name),
|
||||
action='store_const',
|
||||
dest=name,
|
||||
const=default if default is not _empty else False)
|
||||
|
||||
|
||||
def make_parser(func_sig, description, epilog, add_nos):
|
||||
'''
|
||||
Given the signature of a function, create an ArgumentParser
|
||||
'''
|
||||
parser = ArgumentParser(description=description, epilog=epilog)
|
||||
|
||||
used_char_args = {'h'}
|
||||
|
||||
# Arange the params so that single-character arguments are first. This
|
||||
# esnures they don't have to get --long versions. sorted is stable, so the
|
||||
# parameters will otherwise still be in relative order.
|
||||
params = sorted(
|
||||
func_sig.parameters.values(),
|
||||
key=lambda param: len(param.name) > 1)
|
||||
|
||||
for param in params:
|
||||
_add_arguments(param, parser, used_char_args, add_nos)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
_DOCSTRING_SPLIT = compile_regex(r'\n\s*-{4,}\s*\n')
|
||||
|
||||
|
||||
def parse_docstring(docstring):
|
||||
'''
|
||||
Given a docstring, parse it into a description and epilog part
|
||||
'''
|
||||
if docstring is None:
|
||||
return '', ''
|
||||
|
||||
parts = _DOCSTRING_SPLIT.split(docstring)
|
||||
|
||||
if len(parts) == 1:
|
||||
return docstring, ''
|
||||
elif len(parts) == 2:
|
||||
return parts[0], parts[1]
|
||||
else:
|
||||
raise TooManySplitsError()
|
||||
|
||||
|
||||
def autoparse(
|
||||
func=None, *,
|
||||
description=None,
|
||||
epilog=None,
|
||||
add_nos=False,
|
||||
parser=None):
|
||||
'''
|
||||
This decorator converts a function that takes normal arguments into a
|
||||
function which takes a single optional argument, argv, parses it using an
|
||||
argparse.ArgumentParser, and calls the underlying function with the parsed
|
||||
arguments. If it is not given, sys.argv[1:] is used. This is so that the
|
||||
function can be used as a setuptools entry point, as well as a normal main
|
||||
function. sys.argv[1:] is not evaluated until the function is called, to
|
||||
allow injecting different arguments for testing.
|
||||
|
||||
It uses the argument signature of the function to create an
|
||||
ArgumentParser. Parameters without defaults become positional parameters,
|
||||
while parameters *with* defaults become --options. Use annotations to set
|
||||
the type of the parameter.
|
||||
|
||||
The `desctiption` and `epilog` parameters corrospond to the same respective
|
||||
argparse parameters. If no description is given, it defaults to the
|
||||
decorated functions's docstring, if present.
|
||||
|
||||
If add_nos is True, every boolean option (that is, every parameter with a
|
||||
default of True/False or a type of bool) will have a --no- version created
|
||||
as well, which inverts the option. For instance, the --verbose option will
|
||||
have a --no-verbose counterpart. These are not mutually exclusive-
|
||||
whichever one appears last in the argument list will have precedence.
|
||||
|
||||
If a parser is given, it is used instead of one generated from the function
|
||||
signature. In this case, no parser is created; instead, the given parser is
|
||||
used to parse the argv argument. The parser's results' argument names must
|
||||
match up with the parameter names of the decorated function.
|
||||
|
||||
The decorated function is attached to the result as the `func` attribute,
|
||||
and the parser is attached as the `parser` attribute.
|
||||
'''
|
||||
|
||||
# If @autoparse(...) is used instead of @autoparse
|
||||
if func is None:
|
||||
return lambda f: autoparse(
|
||||
f, description=description,
|
||||
epilog=epilog,
|
||||
add_nos=add_nos,
|
||||
parser=parser)
|
||||
|
||||
func_sig = signature(func)
|
||||
|
||||
docstr_description, docstr_epilog = parse_docstring(getdoc(func))
|
||||
|
||||
if parser is None:
|
||||
parser = make_parser(
|
||||
func_sig,
|
||||
description or docstr_description,
|
||||
epilog or docstr_epilog,
|
||||
add_nos)
|
||||
|
||||
@wraps(func)
|
||||
def autoparse_wrapper(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
# Get empty argument binding, to fill with parsed arguments. This
|
||||
# object does all the heavy lifting of turning named arguments into
|
||||
# into correctly bound *args and **kwargs.
|
||||
parsed_args = func_sig.bind_partial()
|
||||
parsed_args.arguments.update(vars(parser.parse_args(argv)))
|
||||
|
||||
return func(*parsed_args.args, **parsed_args.kwargs)
|
||||
|
||||
# TODO: attach an updated __signature__ to autoparse_wrapper, just in case.
|
||||
|
||||
# Attach the wrapped function and parser, and return the wrapper.
|
||||
autoparse_wrapper.func = func
|
||||
autoparse_wrapper.parser = parser
|
||||
return autoparse_wrapper
|
||||
|
||||
|
||||
@contextmanager
|
||||
def smart_open(filename_or_file, *args, **kwargs):
|
||||
'''
|
||||
This context manager allows you to open a filename, if you want to default
|
||||
some already-existing file object, like sys.stdout, which shouldn't be
|
||||
closed at the end of the context. If the filename argument is a str, bytes,
|
||||
or int, the file object is created via a call to open with the given *args
|
||||
and **kwargs, sent to the context, and closed at the end of the context,
|
||||
just like "with open(filename) as f:". If it isn't one of the openable
|
||||
types, the object simply sent to the context unchanged, and left unclosed
|
||||
at the end of the context. Example:
|
||||
|
||||
def work_with_file(name=sys.stdout):
|
||||
with smart_open(name) as f:
|
||||
# Works correctly if name is a str filename or sys.stdout
|
||||
print("Some stuff", file=f)
|
||||
# If it was a filename, f is closed at the end here.
|
||||
'''
|
||||
if isinstance(filename_or_file, (str, bytes, int)):
|
||||
with open(filename_or_file, *args, **kwargs) as file:
|
||||
yield file
|
||||
else:
|
||||
yield filename_or_file
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright 2014-2016 Nathan West
|
||||
#
|
||||
# This file is part of autocommand.
|
||||
#
|
||||
# autocommand is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# autocommand is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class AutocommandError(Exception):
|
||||
'''Base class for autocommand exceptions'''
|
||||
pass
|
||||
|
||||
# Individual modules will define errors specific to that module.
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,17 @@
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
@@ -0,0 +1,46 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: backports.tarfile
|
||||
Version: 1.2.0
|
||||
Summary: Backport of CPython tarfile module
|
||||
Author-email: "Jason R. Coombs" <jaraco@jaraco.com>
|
||||
Project-URL: Homepage, https://github.com/jaraco/backports.tarfile
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: sphinx >=3.5 ; extra == 'docs'
|
||||
Requires-Dist: jaraco.packaging >=9.3 ; extra == 'docs'
|
||||
Requires-Dist: rst.linker >=1.9 ; extra == 'docs'
|
||||
Requires-Dist: furo ; extra == 'docs'
|
||||
Requires-Dist: sphinx-lint ; extra == 'docs'
|
||||
Provides-Extra: testing
|
||||
Requires-Dist: pytest !=8.1.*,>=6 ; extra == 'testing'
|
||||
Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'testing'
|
||||
Requires-Dist: pytest-cov ; extra == 'testing'
|
||||
Requires-Dist: pytest-enabler >=2.2 ; extra == 'testing'
|
||||
Requires-Dist: jaraco.test ; extra == 'testing'
|
||||
Requires-Dist: pytest !=8.0.* ; extra == 'testing'
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/backports.tarfile.svg
|
||||
:target: https://pypi.org/project/backports.tarfile
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/backports.tarfile.svg
|
||||
|
||||
.. image:: https://github.com/jaraco/backports.tarfile/actions/workflows/main.yml/badge.svg
|
||||
:target: https://github.com/jaraco/backports.tarfile/actions?query=workflow%3A%22tests%22
|
||||
:alt: tests
|
||||
|
||||
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
|
||||
:target: https://github.com/astral-sh/ruff
|
||||
:alt: Ruff
|
||||
|
||||
.. .. image:: https://readthedocs.org/projects/backportstarfile/badge/?version=latest
|
||||
.. :target: https://backportstarfile.readthedocs.io/en/latest/?badge=latest
|
||||
|
||||
.. image:: https://img.shields.io/badge/skeleton-2024-informational
|
||||
:target: https://blog.jaraco.com/skeleton
|
||||
@@ -0,0 +1,17 @@
|
||||
backports.tarfile-1.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
backports.tarfile-1.2.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
||||
backports.tarfile-1.2.0.dist-info/METADATA,sha256=ghXFTq132dxaEIolxr3HK1mZqm9iyUmaRANZQSr6WlE,2020
|
||||
backports.tarfile-1.2.0.dist-info/RECORD,,
|
||||
backports.tarfile-1.2.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
backports.tarfile-1.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
||||
backports.tarfile-1.2.0.dist-info/top_level.txt,sha256=cGjaLMOoBR1FK0ApojtzWVmViTtJ7JGIK_HwXiEsvtU,10
|
||||
backports/__init__.py,sha256=iOEMwnlORWezdO8-2vxBIPSR37D7JGjluZ8f55vzxls,81
|
||||
backports/__pycache__/__init__.cpython-312.pyc,,
|
||||
backports/tarfile/__init__.py,sha256=Pwf2qUIfB0SolJPCKcx3vz3UEu_aids4g4sAfxy94qg,108491
|
||||
backports/tarfile/__main__.py,sha256=Yw2oGT1afrz2eBskzdPYL8ReB_3liApmhFkN2EbDmc4,59
|
||||
backports/tarfile/__pycache__/__init__.cpython-312.pyc,,
|
||||
backports/tarfile/__pycache__/__main__.cpython-312.pyc,,
|
||||
backports/tarfile/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
backports/tarfile/compat/__pycache__/__init__.cpython-312.pyc,,
|
||||
backports/tarfile/compat/__pycache__/py38.cpython-312.pyc,,
|
||||
backports/tarfile/compat/py38.py,sha256=iYkyt_gvWjLzGUTJD9TuTfMMjOk-ersXZmRlvQYN2qE,568
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.43.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
backports
|
||||
@@ -0,0 +1 @@
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
from . import main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
|
||||
def removesuffix(self, suffix):
|
||||
# suffix='' should not call self[:-0].
|
||||
if suffix and self.endswith(suffix):
|
||||
return self[: -len(suffix)]
|
||||
else:
|
||||
return self[:]
|
||||
|
||||
def removeprefix(self, prefix):
|
||||
if self.startswith(prefix):
|
||||
return self[len(prefix) :]
|
||||
else:
|
||||
return self[:]
|
||||
else:
|
||||
|
||||
def removesuffix(self, suffix):
|
||||
return self.removesuffix(suffix)
|
||||
|
||||
def removeprefix(self, prefix):
|
||||
return self.removeprefix(prefix)
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,129 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: importlib_metadata
|
||||
Version: 8.0.0
|
||||
Summary: Read metadata from Python packages
|
||||
Author-email: "Jason R. Coombs" <jaraco@jaraco.com>
|
||||
Project-URL: Source, https://github.com/python/importlib_metadata
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: Apache Software License
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: zipp >=0.5
|
||||
Requires-Dist: typing-extensions >=3.6.4 ; python_version < "3.8"
|
||||
Provides-Extra: doc
|
||||
Requires-Dist: sphinx >=3.5 ; extra == 'doc'
|
||||
Requires-Dist: jaraco.packaging >=9.3 ; extra == 'doc'
|
||||
Requires-Dist: rst.linker >=1.9 ; extra == 'doc'
|
||||
Requires-Dist: furo ; extra == 'doc'
|
||||
Requires-Dist: sphinx-lint ; extra == 'doc'
|
||||
Requires-Dist: jaraco.tidelift >=1.4 ; extra == 'doc'
|
||||
Provides-Extra: perf
|
||||
Requires-Dist: ipython ; extra == 'perf'
|
||||
Provides-Extra: test
|
||||
Requires-Dist: pytest !=8.1.*,>=6 ; extra == 'test'
|
||||
Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'test'
|
||||
Requires-Dist: pytest-cov ; extra == 'test'
|
||||
Requires-Dist: pytest-mypy ; extra == 'test'
|
||||
Requires-Dist: pytest-enabler >=2.2 ; extra == 'test'
|
||||
Requires-Dist: pytest-ruff >=0.2.1 ; extra == 'test'
|
||||
Requires-Dist: packaging ; extra == 'test'
|
||||
Requires-Dist: pyfakefs ; extra == 'test'
|
||||
Requires-Dist: flufl.flake8 ; extra == 'test'
|
||||
Requires-Dist: pytest-perf >=0.9.2 ; extra == 'test'
|
||||
Requires-Dist: jaraco.test >=5.4 ; extra == 'test'
|
||||
Requires-Dist: importlib-resources >=1.3 ; (python_version < "3.9") and extra == 'test'
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg
|
||||
:target: https://pypi.org/project/importlib_metadata
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg
|
||||
|
||||
.. image:: https://github.com/python/importlib_metadata/actions/workflows/main.yml/badge.svg
|
||||
:target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22
|
||||
:alt: tests
|
||||
|
||||
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
|
||||
:target: https://github.com/astral-sh/ruff
|
||||
:alt: Ruff
|
||||
|
||||
.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest
|
||||
:target: https://importlib-metadata.readthedocs.io/en/latest/?badge=latest
|
||||
|
||||
.. image:: https://img.shields.io/badge/skeleton-2024-informational
|
||||
:target: https://blog.jaraco.com/skeleton
|
||||
|
||||
.. image:: https://tidelift.com/badges/package/pypi/importlib-metadata
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=readme
|
||||
|
||||
Library to access the metadata for a Python package.
|
||||
|
||||
This package supplies third-party access to the functionality of
|
||||
`importlib.metadata <https://docs.python.org/3/library/importlib.metadata.html>`_
|
||||
including improvements added to subsequent Python versions.
|
||||
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
New features are introduced in this third-party library and later merged
|
||||
into CPython. The following table indicates which versions of this library
|
||||
were contributed to different versions in the standard library:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - importlib_metadata
|
||||
- stdlib
|
||||
* - 7.0
|
||||
- 3.13
|
||||
* - 6.5
|
||||
- 3.12
|
||||
* - 4.13
|
||||
- 3.11
|
||||
* - 4.6
|
||||
- 3.10
|
||||
* - 1.4
|
||||
- 3.8
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
See the `online documentation <https://importlib-metadata.readthedocs.io/>`_
|
||||
for usage details.
|
||||
|
||||
`Finder authors
|
||||
<https://docs.python.org/3/reference/import.html#finders-and-loaders>`_ can
|
||||
also add support for custom package installers. See the above documentation
|
||||
for details.
|
||||
|
||||
|
||||
Caveats
|
||||
=======
|
||||
|
||||
This project primarily supports third-party packages installed by PyPA
|
||||
tools (or other conforming packages). It does not support:
|
||||
|
||||
- Packages in the stdlib.
|
||||
- Packages installed without metadata.
|
||||
|
||||
Project details
|
||||
===============
|
||||
|
||||
* Project home: https://github.com/python/importlib_metadata
|
||||
* Report bugs at: https://github.com/python/importlib_metadata/issues
|
||||
* Code hosting: https://github.com/python/importlib_metadata
|
||||
* Documentation: https://importlib-metadata.readthedocs.io/
|
||||
|
||||
For Enterprise
|
||||
==============
|
||||
|
||||
Available as part of the Tidelift Subscription.
|
||||
|
||||
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
|
||||
|
||||
`Learn more <https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=referral&utm_campaign=github>`_.
|
||||
@@ -0,0 +1,32 @@
|
||||
importlib_metadata-8.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
importlib_metadata-8.0.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
||||
importlib_metadata-8.0.0.dist-info/METADATA,sha256=anuQ7_7h4J1bSEzfcjIBakPi2cyVQ7y7jklLHsBeH1k,4648
|
||||
importlib_metadata-8.0.0.dist-info/RECORD,,
|
||||
importlib_metadata-8.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
importlib_metadata-8.0.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
|
||||
importlib_metadata-8.0.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
|
||||
importlib_metadata/__init__.py,sha256=tZNB-23h8Bixi9uCrQqj9Yf0aeC--Josdy3IZRIQeB0,33798
|
||||
importlib_metadata/__pycache__/__init__.cpython-312.pyc,,
|
||||
importlib_metadata/__pycache__/_adapters.cpython-312.pyc,,
|
||||
importlib_metadata/__pycache__/_collections.cpython-312.pyc,,
|
||||
importlib_metadata/__pycache__/_compat.cpython-312.pyc,,
|
||||
importlib_metadata/__pycache__/_functools.cpython-312.pyc,,
|
||||
importlib_metadata/__pycache__/_itertools.cpython-312.pyc,,
|
||||
importlib_metadata/__pycache__/_meta.cpython-312.pyc,,
|
||||
importlib_metadata/__pycache__/_text.cpython-312.pyc,,
|
||||
importlib_metadata/__pycache__/diagnose.cpython-312.pyc,,
|
||||
importlib_metadata/_adapters.py,sha256=rIhWTwBvYA1bV7i-5FfVX38qEXDTXFeS5cb5xJtP3ks,2317
|
||||
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
|
||||
importlib_metadata/_compat.py,sha256=73QKrN9KNoaZzhbX5yPCCZa-FaALwXe8TPlDR72JgBU,1314
|
||||
importlib_metadata/_functools.py,sha256=PsY2-4rrKX4RVeRC1oGp1lB1pmC9eKN88_f-bD9uOoA,2895
|
||||
importlib_metadata/_itertools.py,sha256=cvr_2v8BRbxcIl5x5ldfqdHjhI8Yi8s8yk50G_nm6jQ,2068
|
||||
importlib_metadata/_meta.py,sha256=nxZ7C8GVlcBFAKWyVOn_dn7ot_twBcbm1NmvjIetBHI,1801
|
||||
importlib_metadata/_text.py,sha256=HCsFksZpJLeTP3NEk_ngrAeXVRRtTrtyh9eOABoRP4A,2166
|
||||
importlib_metadata/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
importlib_metadata/compat/__pycache__/__init__.cpython-312.pyc,,
|
||||
importlib_metadata/compat/__pycache__/py311.cpython-312.pyc,,
|
||||
importlib_metadata/compat/__pycache__/py39.cpython-312.pyc,,
|
||||
importlib_metadata/compat/py311.py,sha256=uqm-K-uohyj1042TH4a9Er_I5o7667DvulcD-gC_fSA,608
|
||||
importlib_metadata/compat/py39.py,sha256=cPkMv6-0ilK-0Jw_Tkn0xYbOKJZc4WJKQHow0c2T44w,1102
|
||||
importlib_metadata/diagnose.py,sha256=nkSRMiowlmkhLYhKhvCg9glmt_11Cox-EmLzEbqYTa8,379
|
||||
importlib_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (70.1.1)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
importlib_metadata
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user