删除.venv
重构为RL和IL两部分内容。
This commit is contained in:
@@ -1,239 +0,0 @@
|
|||||||
# don't import any costly modules
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
report_url = (
|
|
||||||
"https://github.com/pypa/setuptools/issues/new?template=distutils-deprecation.yml"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def warn_distutils_present():
|
|
||||||
if 'distutils' not in sys.modules:
|
|
||||||
return
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
|
||||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
|
||||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
|
||||||
"using distutils directly, ensure that setuptools is installed in the "
|
|
||||||
"traditional way (e.g. not an editable install), and/or make sure "
|
|
||||||
"that setuptools is always imported before distutils."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def clear_distutils():
|
|
||||||
if 'distutils' not in sys.modules:
|
|
||||||
return
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Setuptools is replacing distutils. Support for replacing "
|
|
||||||
"an already imported distutils is deprecated. In the future, "
|
|
||||||
"this condition will fail. "
|
|
||||||
f"Register concerns at {report_url}"
|
|
||||||
)
|
|
||||||
mods = [
|
|
||||||
name
|
|
||||||
for name in sys.modules
|
|
||||||
if name == "distutils" or name.startswith("distutils.")
|
|
||||||
]
|
|
||||||
for name in mods:
|
|
||||||
del sys.modules[name]
|
|
||||||
|
|
||||||
|
|
||||||
def enabled():
|
|
||||||
"""
|
|
||||||
Allow selection of distutils by environment variable.
|
|
||||||
"""
|
|
||||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
|
||||||
if which == 'stdlib':
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Reliance on distutils from stdlib is deprecated. Users "
|
|
||||||
"must rely on setuptools to provide the distutils module. "
|
|
||||||
"Avoid importing distutils or import setuptools first, "
|
|
||||||
"and avoid setting SETUPTOOLS_USE_DISTUTILS=stdlib. "
|
|
||||||
f"Register concerns at {report_url}"
|
|
||||||
)
|
|
||||||
return which == 'local'
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_local_distutils():
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
clear_distutils()
|
|
||||||
|
|
||||||
# With the DistutilsMetaFinder in place,
|
|
||||||
# perform an import to cause distutils to be
|
|
||||||
# loaded from setuptools._distutils. Ref #2906.
|
|
||||||
with shim():
|
|
||||||
importlib.import_module('distutils')
|
|
||||||
|
|
||||||
# check that submodules load as expected
|
|
||||||
core = importlib.import_module('distutils.core')
|
|
||||||
assert '_distutils' in core.__file__, core.__file__
|
|
||||||
assert 'setuptools._distutils.log' not in sys.modules
|
|
||||||
|
|
||||||
|
|
||||||
def do_override():
|
|
||||||
"""
|
|
||||||
Ensure that the local copy of distutils is preferred over stdlib.
|
|
||||||
|
|
||||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
|
||||||
for more motivation.
|
|
||||||
"""
|
|
||||||
if enabled():
|
|
||||||
warn_distutils_present()
|
|
||||||
ensure_local_distutils()
|
|
||||||
|
|
||||||
|
|
||||||
class _TrivialRe:
|
|
||||||
def __init__(self, *patterns) -> None:
|
|
||||||
self._patterns = patterns
|
|
||||||
|
|
||||||
def match(self, string):
|
|
||||||
return all(pat in string for pat in self._patterns)
|
|
||||||
|
|
||||||
|
|
||||||
class DistutilsMetaFinder:
|
|
||||||
def find_spec(self, fullname, path, target=None):
|
|
||||||
# optimization: only consider top level modules and those
|
|
||||||
# found in the CPython test suite.
|
|
||||||
if path is not None and not fullname.startswith('test.'):
|
|
||||||
return None
|
|
||||||
|
|
||||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
|
||||||
method = getattr(self, method_name, lambda: None)
|
|
||||||
return method()
|
|
||||||
|
|
||||||
def spec_for_distutils(self):
|
|
||||||
if self.is_cpython():
|
|
||||||
return None
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
import importlib.abc
|
|
||||||
import importlib.util
|
|
||||||
|
|
||||||
try:
|
|
||||||
mod = importlib.import_module('setuptools._distutils')
|
|
||||||
except Exception:
|
|
||||||
# There are a couple of cases where setuptools._distutils
|
|
||||||
# may not be present:
|
|
||||||
# - An older Setuptools without a local distutils is
|
|
||||||
# taking precedence. Ref #2957.
|
|
||||||
# - Path manipulation during sitecustomize removes
|
|
||||||
# setuptools from the path but only after the hook
|
|
||||||
# has been loaded. Ref #2980.
|
|
||||||
# In either case, fall back to stdlib behavior.
|
|
||||||
return None
|
|
||||||
|
|
||||||
class DistutilsLoader(importlib.abc.Loader):
|
|
||||||
def create_module(self, spec):
|
|
||||||
mod.__name__ = 'distutils'
|
|
||||||
return mod
|
|
||||||
|
|
||||||
def exec_module(self, module):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return importlib.util.spec_from_loader(
|
|
||||||
'distutils', DistutilsLoader(), origin=mod.__file__
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_cpython():
|
|
||||||
"""
|
|
||||||
Suppress supplying distutils for CPython (build and tests).
|
|
||||||
Ref #2965 and #3007.
|
|
||||||
"""
|
|
||||||
return os.path.isfile('pybuilddir.txt')
|
|
||||||
|
|
||||||
def spec_for_pip(self):
|
|
||||||
"""
|
|
||||||
Ensure stdlib distutils when running under pip.
|
|
||||||
See pypa/pip#8761 for rationale.
|
|
||||||
"""
|
|
||||||
if sys.version_info >= (3, 12) or self.pip_imported_during_build():
|
|
||||||
return
|
|
||||||
clear_distutils()
|
|
||||||
self.spec_for_distutils = lambda: None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def pip_imported_during_build(cls):
|
|
||||||
"""
|
|
||||||
Detect if pip is being imported in a build script. Ref #2355.
|
|
||||||
"""
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
return any(
|
|
||||||
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def frame_file_is_setup(frame):
|
|
||||||
"""
|
|
||||||
Return True if the indicated frame suggests a setup.py file.
|
|
||||||
"""
|
|
||||||
# some frames may not have __file__ (#2940)
|
|
||||||
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
|
||||||
|
|
||||||
def spec_for_sensitive_tests(self):
|
|
||||||
"""
|
|
||||||
Ensure stdlib distutils when running select tests under CPython.
|
|
||||||
|
|
||||||
python/cpython#91169
|
|
||||||
"""
|
|
||||||
clear_distutils()
|
|
||||||
self.spec_for_distutils = lambda: None
|
|
||||||
|
|
||||||
sensitive_tests = (
|
|
||||||
[
|
|
||||||
'test.test_distutils',
|
|
||||||
'test.test_peg_generator',
|
|
||||||
'test.test_importlib',
|
|
||||||
]
|
|
||||||
if sys.version_info < (3, 10)
|
|
||||||
else [
|
|
||||||
'test.test_distutils',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
for name in DistutilsMetaFinder.sensitive_tests:
|
|
||||||
setattr(
|
|
||||||
DistutilsMetaFinder,
|
|
||||||
f'spec_for_{name}',
|
|
||||||
DistutilsMetaFinder.spec_for_sensitive_tests,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
|
||||||
|
|
||||||
|
|
||||||
def add_shim():
|
|
||||||
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
|
||||||
|
|
||||||
|
|
||||||
class shim:
|
|
||||||
def __enter__(self) -> None:
|
|
||||||
insert_shim()
|
|
||||||
|
|
||||||
def __exit__(self, exc: object, value: object, tb: object) -> None:
|
|
||||||
_remove_shim()
|
|
||||||
|
|
||||||
|
|
||||||
def insert_shim():
|
|
||||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_shim():
|
|
||||||
try:
|
|
||||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 12):
|
|
||||||
# DistutilsMetaFinder can only be disabled in Python < 3.12 (PEP 632)
|
|
||||||
remove_shim = _remove_shim
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
__import__('_distutils_hack').do_override()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import os; var = 'SETUPTOOLS_USE_DISTUTILS'; enabled = os.environ.get(var, 'local') == 'local'; enabled and __import__('_distutils_hack').add_shim();
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,424 +0,0 @@
|
|||||||
Pluggable Distributions of Python Software
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
Distributions
|
|
||||||
-------------
|
|
||||||
|
|
||||||
A "Distribution" is a collection of files that represent a "Release" of a
|
|
||||||
"Project" as of a particular point in time, denoted by a
|
|
||||||
"Version"::
|
|
||||||
|
|
||||||
>>> import sys, pkg_resources
|
|
||||||
>>> from pkg_resources import Distribution
|
|
||||||
>>> Distribution(project_name="Foo", version="1.2")
|
|
||||||
Foo 1.2
|
|
||||||
|
|
||||||
Distributions have a location, which can be a filename, URL, or really anything
|
|
||||||
else you care to use::
|
|
||||||
|
|
||||||
>>> dist = Distribution(
|
|
||||||
... location="http://example.com/something",
|
|
||||||
... project_name="Bar", version="0.9"
|
|
||||||
... )
|
|
||||||
|
|
||||||
>>> dist
|
|
||||||
Bar 0.9 (http://example.com/something)
|
|
||||||
|
|
||||||
|
|
||||||
Distributions have various introspectable attributes::
|
|
||||||
|
|
||||||
>>> dist.location
|
|
||||||
'http://example.com/something'
|
|
||||||
|
|
||||||
>>> dist.project_name
|
|
||||||
'Bar'
|
|
||||||
|
|
||||||
>>> dist.version
|
|
||||||
'0.9'
|
|
||||||
|
|
||||||
>>> dist.py_version == '{}.{}'.format(*sys.version_info)
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> print(dist.platform)
|
|
||||||
None
|
|
||||||
|
|
||||||
Including various computed attributes::
|
|
||||||
|
|
||||||
>>> from pkg_resources import parse_version
|
|
||||||
>>> dist.parsed_version == parse_version(dist.version)
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> dist.key # case-insensitive form of the project name
|
|
||||||
'bar'
|
|
||||||
|
|
||||||
Distributions are compared (and hashed) by version first::
|
|
||||||
|
|
||||||
>>> Distribution(version='1.0') == Distribution(version='1.0')
|
|
||||||
True
|
|
||||||
>>> Distribution(version='1.0') == Distribution(version='1.1')
|
|
||||||
False
|
|
||||||
>>> Distribution(version='1.0') < Distribution(version='1.1')
|
|
||||||
True
|
|
||||||
|
|
||||||
but also by project name (case-insensitive), platform, Python version,
|
|
||||||
location, etc.::
|
|
||||||
|
|
||||||
>>> Distribution(project_name="Foo",version="1.0") == \
|
|
||||||
... Distribution(project_name="Foo",version="1.0")
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> Distribution(project_name="Foo",version="1.0") == \
|
|
||||||
... Distribution(project_name="foo",version="1.0")
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> Distribution(project_name="Foo",version="1.0") == \
|
|
||||||
... Distribution(project_name="Foo",version="1.1")
|
|
||||||
False
|
|
||||||
|
|
||||||
>>> Distribution(project_name="Foo",py_version="2.3",version="1.0") == \
|
|
||||||
... Distribution(project_name="Foo",py_version="2.4",version="1.0")
|
|
||||||
False
|
|
||||||
|
|
||||||
>>> Distribution(location="spam",version="1.0") == \
|
|
||||||
... Distribution(location="spam",version="1.0")
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> Distribution(location="spam",version="1.0") == \
|
|
||||||
... Distribution(location="baz",version="1.0")
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Hash and compare distribution by prio/plat
|
|
||||||
|
|
||||||
Get version from metadata
|
|
||||||
provider capabilities
|
|
||||||
egg_name()
|
|
||||||
as_requirement()
|
|
||||||
from_location, from_filename (w/path normalization)
|
|
||||||
|
|
||||||
Releases may have zero or more "Requirements", which indicate
|
|
||||||
what releases of another project the release requires in order to
|
|
||||||
function. A Requirement names the other project, expresses some criteria
|
|
||||||
as to what releases of that project are acceptable, and lists any "Extras"
|
|
||||||
that the requiring release may need from that project. (An Extra is an
|
|
||||||
optional feature of a Release, that can only be used if its additional
|
|
||||||
Requirements are satisfied.)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The Working Set
|
|
||||||
---------------
|
|
||||||
|
|
||||||
A collection of active distributions is called a Working Set. Note that a
|
|
||||||
Working Set can contain any importable distribution, not just pluggable ones.
|
|
||||||
For example, the Python standard library is an importable distribution that
|
|
||||||
will usually be part of the Working Set, even though it is not pluggable.
|
|
||||||
Similarly, when you are doing development work on a project, the files you are
|
|
||||||
editing are also a Distribution. (And, with a little attention to the
|
|
||||||
directory names used, and including some additional metadata, such a
|
|
||||||
"development distribution" can be made pluggable as well.)
|
|
||||||
|
|
||||||
>>> from pkg_resources import WorkingSet
|
|
||||||
|
|
||||||
A working set's entries are the sys.path entries that correspond to the active
|
|
||||||
distributions. By default, the working set's entries are the items on
|
|
||||||
``sys.path``::
|
|
||||||
|
|
||||||
>>> ws = WorkingSet()
|
|
||||||
>>> ws.entries == sys.path
|
|
||||||
True
|
|
||||||
|
|
||||||
But you can also create an empty working set explicitly, and add distributions
|
|
||||||
to it::
|
|
||||||
|
|
||||||
>>> ws = WorkingSet([])
|
|
||||||
>>> ws.add(dist)
|
|
||||||
>>> ws.entries
|
|
||||||
['http://example.com/something']
|
|
||||||
>>> dist in ws
|
|
||||||
True
|
|
||||||
>>> Distribution('foo',version="") in ws
|
|
||||||
False
|
|
||||||
|
|
||||||
And you can iterate over its distributions::
|
|
||||||
|
|
||||||
>>> list(ws)
|
|
||||||
[Bar 0.9 (http://example.com/something)]
|
|
||||||
|
|
||||||
Adding the same distribution more than once is a no-op::
|
|
||||||
|
|
||||||
>>> ws.add(dist)
|
|
||||||
>>> list(ws)
|
|
||||||
[Bar 0.9 (http://example.com/something)]
|
|
||||||
|
|
||||||
For that matter, adding multiple distributions for the same project also does
|
|
||||||
nothing, because a working set can only hold one active distribution per
|
|
||||||
project -- the first one added to it::
|
|
||||||
|
|
||||||
>>> ws.add(
|
|
||||||
... Distribution(
|
|
||||||
... 'http://example.com/something', project_name="Bar",
|
|
||||||
... version="7.2"
|
|
||||||
... )
|
|
||||||
... )
|
|
||||||
>>> list(ws)
|
|
||||||
[Bar 0.9 (http://example.com/something)]
|
|
||||||
|
|
||||||
You can append a path entry to a working set using ``add_entry()``::
|
|
||||||
|
|
||||||
>>> ws.entries
|
|
||||||
['http://example.com/something']
|
|
||||||
>>> ws.add_entry(pkg_resources.__file__)
|
|
||||||
>>> ws.entries
|
|
||||||
['http://example.com/something', '...pkg_resources...']
|
|
||||||
|
|
||||||
Multiple additions result in multiple entries, even if the entry is already in
|
|
||||||
the working set (because ``sys.path`` can contain the same entry more than
|
|
||||||
once)::
|
|
||||||
|
|
||||||
>>> ws.add_entry(pkg_resources.__file__)
|
|
||||||
>>> ws.entries
|
|
||||||
['...example.com...', '...pkg_resources...', '...pkg_resources...']
|
|
||||||
|
|
||||||
And you can specify the path entry a distribution was found under, using the
|
|
||||||
optional second parameter to ``add()``::
|
|
||||||
|
|
||||||
>>> ws = WorkingSet([])
|
|
||||||
>>> ws.add(dist,"foo")
|
|
||||||
>>> ws.entries
|
|
||||||
['foo']
|
|
||||||
|
|
||||||
But even if a distribution is found under multiple path entries, it still only
|
|
||||||
shows up once when iterating the working set:
|
|
||||||
|
|
||||||
>>> ws.add_entry(ws.entries[0])
|
|
||||||
>>> list(ws)
|
|
||||||
[Bar 0.9 (http://example.com/something)]
|
|
||||||
|
|
||||||
You can ask a WorkingSet to ``find()`` a distribution matching a requirement::
|
|
||||||
|
|
||||||
>>> from pkg_resources import Requirement
|
|
||||||
>>> print(ws.find(Requirement.parse("Foo==1.0"))) # no match, return None
|
|
||||||
None
|
|
||||||
|
|
||||||
>>> ws.find(Requirement.parse("Bar==0.9")) # match, return distribution
|
|
||||||
Bar 0.9 (http://example.com/something)
|
|
||||||
|
|
||||||
Note that asking for a conflicting version of a distribution already in a
|
|
||||||
working set triggers a ``pkg_resources.VersionConflict`` error:
|
|
||||||
|
|
||||||
>>> try:
|
|
||||||
... ws.find(Requirement.parse("Bar==1.0"))
|
|
||||||
... except pkg_resources.VersionConflict as exc:
|
|
||||||
... print(str(exc))
|
|
||||||
... else:
|
|
||||||
... raise AssertionError("VersionConflict was not raised")
|
|
||||||
(Bar 0.9 (http://example.com/something), Requirement.parse('Bar==1.0'))
|
|
||||||
|
|
||||||
You can subscribe a callback function to receive notifications whenever a new
|
|
||||||
distribution is added to a working set. The callback is immediately invoked
|
|
||||||
once for each existing distribution in the working set, and then is called
|
|
||||||
again for new distributions added thereafter::
|
|
||||||
|
|
||||||
>>> def added(dist): print("Added %s" % dist)
|
|
||||||
>>> ws.subscribe(added)
|
|
||||||
Added Bar 0.9
|
|
||||||
>>> foo12 = Distribution(project_name="Foo", version="1.2", location="f12")
|
|
||||||
>>> ws.add(foo12)
|
|
||||||
Added Foo 1.2
|
|
||||||
|
|
||||||
Note, however, that only the first distribution added for a given project name
|
|
||||||
will trigger a callback, even during the initial ``subscribe()`` callback::
|
|
||||||
|
|
||||||
>>> foo14 = Distribution(project_name="Foo", version="1.4", location="f14")
|
|
||||||
>>> ws.add(foo14) # no callback, because Foo 1.2 is already active
|
|
||||||
|
|
||||||
>>> ws = WorkingSet([])
|
|
||||||
>>> ws.add(foo12)
|
|
||||||
>>> ws.add(foo14)
|
|
||||||
>>> ws.subscribe(added)
|
|
||||||
Added Foo 1.2
|
|
||||||
|
|
||||||
And adding a callback more than once has no effect, either::
|
|
||||||
|
|
||||||
>>> ws.subscribe(added) # no callbacks
|
|
||||||
|
|
||||||
# and no double-callbacks on subsequent additions, either
|
|
||||||
>>> just_a_test = Distribution(project_name="JustATest", version="0.99")
|
|
||||||
>>> ws.add(just_a_test)
|
|
||||||
Added JustATest 0.99
|
|
||||||
|
|
||||||
|
|
||||||
Finding Plugins
|
|
||||||
---------------
|
|
||||||
|
|
||||||
``WorkingSet`` objects can be used to figure out what plugins in an
|
|
||||||
``Environment`` can be loaded without any resolution errors::
|
|
||||||
|
|
||||||
>>> from pkg_resources import Environment
|
|
||||||
|
|
||||||
>>> plugins = Environment([]) # normally, a list of plugin directories
|
|
||||||
>>> plugins.add(foo12)
|
|
||||||
>>> plugins.add(foo14)
|
|
||||||
>>> plugins.add(just_a_test)
|
|
||||||
|
|
||||||
In the simplest case, we just get the newest version of each distribution in
|
|
||||||
the plugin environment::
|
|
||||||
|
|
||||||
>>> ws = WorkingSet([])
|
|
||||||
>>> ws.find_plugins(plugins)
|
|
||||||
([JustATest 0.99, Foo 1.4 (f14)], {})
|
|
||||||
|
|
||||||
But if there's a problem with a version conflict or missing requirements, the
|
|
||||||
method falls back to older versions, and the error info dict will contain an
|
|
||||||
exception instance for each unloadable plugin::
|
|
||||||
|
|
||||||
>>> ws.add(foo12) # this will conflict with Foo 1.4
|
|
||||||
>>> ws.find_plugins(plugins)
|
|
||||||
([JustATest 0.99, Foo 1.2 (f12)], {Foo 1.4 (f14): VersionConflict(...)})
|
|
||||||
|
|
||||||
But if you disallow fallbacks, the failed plugin will be skipped instead of
|
|
||||||
trying older versions::
|
|
||||||
|
|
||||||
>>> ws.find_plugins(plugins, fallback=False)
|
|
||||||
([JustATest 0.99], {Foo 1.4 (f14): VersionConflict(...)})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Platform Compatibility Rules
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
On the Mac, there are potential compatibility issues for modules compiled
|
|
||||||
on newer versions of macOS than what the user is running. Additionally,
|
|
||||||
macOS will soon have two platforms to contend with: Intel and PowerPC.
|
|
||||||
|
|
||||||
Basic equality works as on other platforms::
|
|
||||||
|
|
||||||
>>> from pkg_resources import compatible_platforms as cp
|
|
||||||
>>> reqd = 'macosx-10.4-ppc'
|
|
||||||
>>> cp(reqd, reqd)
|
|
||||||
True
|
|
||||||
>>> cp("win32", reqd)
|
|
||||||
False
|
|
||||||
|
|
||||||
Distributions made on other machine types are not compatible::
|
|
||||||
|
|
||||||
>>> cp("macosx-10.4-i386", reqd)
|
|
||||||
False
|
|
||||||
|
|
||||||
Distributions made on earlier versions of the OS are compatible, as
|
|
||||||
long as they are from the same top-level version. The patchlevel version
|
|
||||||
number does not matter::
|
|
||||||
|
|
||||||
>>> cp("macosx-10.4-ppc", reqd)
|
|
||||||
True
|
|
||||||
>>> cp("macosx-10.3-ppc", reqd)
|
|
||||||
True
|
|
||||||
>>> cp("macosx-10.5-ppc", reqd)
|
|
||||||
False
|
|
||||||
>>> cp("macosx-9.5-ppc", reqd)
|
|
||||||
False
|
|
||||||
|
|
||||||
Backwards compatibility for packages made via earlier versions of
|
|
||||||
setuptools is provided as well::
|
|
||||||
|
|
||||||
>>> cp("darwin-8.2.0-Power_Macintosh", reqd)
|
|
||||||
True
|
|
||||||
>>> cp("darwin-7.2.0-Power_Macintosh", reqd)
|
|
||||||
True
|
|
||||||
>>> cp("darwin-8.2.0-Power_Macintosh", "macosx-10.3-ppc")
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
Environment Markers
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
>>> from pkg_resources import invalid_marker as im, evaluate_marker as em
|
|
||||||
>>> import os
|
|
||||||
|
|
||||||
>>> print(im("sys_platform"))
|
|
||||||
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
||||||
sys_platform
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("sys_platform=="))
|
|
||||||
Expected a marker variable or quoted string
|
|
||||||
sys_platform==
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("sys_platform=='win32'"))
|
|
||||||
False
|
|
||||||
|
|
||||||
>>> print(im("sys=='x'"))
|
|
||||||
Expected a marker variable or quoted string
|
|
||||||
sys=='x'
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("(extra)"))
|
|
||||||
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
||||||
(extra)
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("(extra"))
|
|
||||||
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
||||||
(extra
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("os.open('foo')=='y'"))
|
|
||||||
Expected a marker variable or quoted string
|
|
||||||
os.open('foo')=='y'
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("'x'=='y' and os.open('foo')=='y'")) # no short-circuit!
|
|
||||||
Expected a marker variable or quoted string
|
|
||||||
'x'=='y' and os.open('foo')=='y'
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("'x'=='x' or os.open('foo')=='y'")) # no short-circuit!
|
|
||||||
Expected a marker variable or quoted string
|
|
||||||
'x'=='x' or os.open('foo')=='y'
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("r'x'=='x'"))
|
|
||||||
Expected a marker variable or quoted string
|
|
||||||
r'x'=='x'
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("'''x'''=='x'"))
|
|
||||||
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
||||||
'''x'''=='x'
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im('"""x"""=="x"'))
|
|
||||||
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
|
|
||||||
"""x"""=="x"
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im(r"x\n=='x'"))
|
|
||||||
Expected a marker variable or quoted string
|
|
||||||
x\n=='x'
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> print(im("os.open=='y'"))
|
|
||||||
Expected a marker variable or quoted string
|
|
||||||
os.open=='y'
|
|
||||||
^
|
|
||||||
|
|
||||||
>>> em("sys_platform=='win32'") == (sys.platform=='win32')
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> em("python_version >= '2.7'")
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> em("python_version > '2.6'")
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> im("implementation_name=='cpython'")
|
|
||||||
False
|
|
||||||
|
|
||||||
>>> im("platform_python_implementation=='CPython'")
|
|
||||||
False
|
|
||||||
|
|
||||||
>>> im("implementation_version=='3.5.1'")
|
|
||||||
False
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
Metadata-Version: 2.4
|
|
||||||
Name: setuptools
|
|
||||||
Version: 79.0.1
|
|
||||||
Summary: Easily download, build, install, upgrade, and uninstall Python packages
|
|
||||||
Author-email: Python Packaging Authority <distutils-sig@python.org>
|
|
||||||
Project-URL: Source, https://github.com/pypa/setuptools
|
|
||||||
Project-URL: Documentation, https://setuptools.pypa.io/
|
|
||||||
Project-URL: Changelog, https://setuptools.pypa.io/en/stable/history.html
|
|
||||||
Keywords: CPAN PyPI distutils eggs package management
|
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
|
||||||
Classifier: Intended Audience :: Developers
|
|
||||||
Classifier: Programming Language :: Python :: 3
|
|
||||||
Classifier: Programming Language :: Python :: 3 :: Only
|
|
||||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
||||||
Classifier: Topic :: System :: Archiving :: Packaging
|
|
||||||
Classifier: Topic :: System :: Systems Administration
|
|
||||||
Classifier: Topic :: Utilities
|
|
||||||
Requires-Python: >=3.9
|
|
||||||
Description-Content-Type: text/x-rst
|
|
||||||
License-File: LICENSE
|
|
||||||
Provides-Extra: test
|
|
||||||
Requires-Dist: pytest!=8.1.*,>=6; extra == "test"
|
|
||||||
Requires-Dist: virtualenv>=13.0.0; extra == "test"
|
|
||||||
Requires-Dist: wheel>=0.44.0; extra == "test"
|
|
||||||
Requires-Dist: pip>=19.1; extra == "test"
|
|
||||||
Requires-Dist: packaging>=24.2; extra == "test"
|
|
||||||
Requires-Dist: jaraco.envs>=2.2; extra == "test"
|
|
||||||
Requires-Dist: pytest-xdist>=3; extra == "test"
|
|
||||||
Requires-Dist: jaraco.path>=3.7.2; extra == "test"
|
|
||||||
Requires-Dist: build[virtualenv]>=1.0.3; extra == "test"
|
|
||||||
Requires-Dist: filelock>=3.4.0; extra == "test"
|
|
||||||
Requires-Dist: ini2toml[lite]>=0.14; extra == "test"
|
|
||||||
Requires-Dist: tomli-w>=1.0.0; extra == "test"
|
|
||||||
Requires-Dist: pytest-timeout; extra == "test"
|
|
||||||
Requires-Dist: pytest-perf; sys_platform != "cygwin" and extra == "test"
|
|
||||||
Requires-Dist: jaraco.develop>=7.21; (python_version >= "3.9" and sys_platform != "cygwin") and extra == "test"
|
|
||||||
Requires-Dist: pytest-home>=0.5; extra == "test"
|
|
||||||
Requires-Dist: pytest-subprocess; extra == "test"
|
|
||||||
Requires-Dist: pyproject-hooks!=1.1; extra == "test"
|
|
||||||
Requires-Dist: jaraco.test>=5.5; extra == "test"
|
|
||||||
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"
|
|
||||||
Requires-Dist: pygments-github-lexers==0.0.5; extra == "doc"
|
|
||||||
Requires-Dist: sphinx-favicon; extra == "doc"
|
|
||||||
Requires-Dist: sphinx-inline-tabs; extra == "doc"
|
|
||||||
Requires-Dist: sphinx-reredirects; extra == "doc"
|
|
||||||
Requires-Dist: sphinxcontrib-towncrier; extra == "doc"
|
|
||||||
Requires-Dist: sphinx-notfound-page<2,>=1; extra == "doc"
|
|
||||||
Requires-Dist: pyproject-hooks!=1.1; extra == "doc"
|
|
||||||
Requires-Dist: towncrier<24.7; extra == "doc"
|
|
||||||
Provides-Extra: ssl
|
|
||||||
Provides-Extra: certs
|
|
||||||
Provides-Extra: core
|
|
||||||
Requires-Dist: packaging>=24.2; extra == "core"
|
|
||||||
Requires-Dist: more_itertools>=8.8; extra == "core"
|
|
||||||
Requires-Dist: jaraco.text>=3.7; extra == "core"
|
|
||||||
Requires-Dist: importlib_metadata>=6; python_version < "3.10" and extra == "core"
|
|
||||||
Requires-Dist: tomli>=2.0.1; python_version < "3.11" and extra == "core"
|
|
||||||
Requires-Dist: wheel>=0.43.0; extra == "core"
|
|
||||||
Requires-Dist: platformdirs>=4.2.2; extra == "core"
|
|
||||||
Requires-Dist: jaraco.functools>=4; extra == "core"
|
|
||||||
Requires-Dist: more_itertools; extra == "core"
|
|
||||||
Provides-Extra: check
|
|
||||||
Requires-Dist: pytest-checkdocs>=2.4; extra == "check"
|
|
||||||
Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check"
|
|
||||||
Requires-Dist: ruff>=0.8.0; sys_platform != "cygwin" and extra == "check"
|
|
||||||
Provides-Extra: cover
|
|
||||||
Requires-Dist: pytest-cov; extra == "cover"
|
|
||||||
Provides-Extra: enabler
|
|
||||||
Requires-Dist: pytest-enabler>=2.2; extra == "enabler"
|
|
||||||
Provides-Extra: type
|
|
||||||
Requires-Dist: pytest-mypy; extra == "type"
|
|
||||||
Requires-Dist: mypy==1.14.*; extra == "type"
|
|
||||||
Requires-Dist: importlib_metadata>=7.0.2; python_version < "3.10" and extra == "type"
|
|
||||||
Requires-Dist: jaraco.develop>=7.21; sys_platform != "cygwin" and extra == "type"
|
|
||||||
Dynamic: license-file
|
|
||||||
|
|
||||||
.. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg
|
|
||||||
:target: https://pypi.org/project/setuptools
|
|
||||||
|
|
||||||
.. |py-version| image:: https://img.shields.io/pypi/pyversions/setuptools.svg
|
|
||||||
|
|
||||||
.. |test-badge| image:: https://github.com/pypa/setuptools/actions/workflows/main.yml/badge.svg
|
|
||||||
:target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22
|
|
||||||
:alt: tests
|
|
||||||
|
|
||||||
.. |ruff-badge| 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
|
|
||||||
|
|
||||||
.. |docs-badge| image:: https://img.shields.io/readthedocs/setuptools/latest.svg
|
|
||||||
:target: https://setuptools.pypa.io
|
|
||||||
|
|
||||||
.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2025-informational
|
|
||||||
:target: https://blog.jaraco.com/skeleton
|
|
||||||
|
|
||||||
.. |codecov-badge| image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white
|
|
||||||
:target: https://codecov.io/gh/pypa/setuptools
|
|
||||||
|
|
||||||
.. |tidelift-badge| image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat
|
|
||||||
:target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme
|
|
||||||
|
|
||||||
.. |discord-badge| image:: https://img.shields.io/discord/803025117553754132
|
|
||||||
:target: https://discord.com/channels/803025117553754132/815945031150993468
|
|
||||||
:alt: Discord
|
|
||||||
|
|
||||||
|pypi-version| |py-version| |test-badge| |ruff-badge| |docs-badge| |skeleton-badge| |codecov-badge| |discord-badge|
|
|
||||||
|
|
||||||
See the `Quickstart <https://setuptools.pypa.io/en/latest/userguide/quickstart.html>`_
|
|
||||||
and the `User's Guide <https://setuptools.pypa.io/en/latest/userguide/>`_ for
|
|
||||||
instructions on how to use Setuptools.
|
|
||||||
|
|
||||||
Questions and comments should be directed to `GitHub Discussions
|
|
||||||
<https://github.com/pypa/setuptools/discussions>`_.
|
|
||||||
Bug reports and especially tested patches may be
|
|
||||||
submitted directly to the `bug tracker
|
|
||||||
<https://github.com/pypa/setuptools/issues>`_.
|
|
||||||
|
|
||||||
|
|
||||||
Code of Conduct
|
|
||||||
===============
|
|
||||||
|
|
||||||
Everyone interacting in the setuptools project's codebases, issue trackers,
|
|
||||||
chat rooms, and fora is expected to follow the
|
|
||||||
`PSF Code of Conduct <https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md>`_.
|
|
||||||
|
|
||||||
|
|
||||||
For Enterprise
|
|
||||||
==============
|
|
||||||
|
|
||||||
Available as part of the Tidelift Subscription.
|
|
||||||
|
|
||||||
Setuptools 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-setuptools?utm_source=pypi-setuptools&utm_medium=referral&utm_campaign=github>`_.
|
|
||||||
@@ -1,514 +0,0 @@
|
|||||||
distutils-precedence.pth,sha256=JjjOniUA5XKl4N5_rtZmHrVp0baW_LoHsN0iPaX10iQ,151
|
|
||||||
_distutils_hack/__init__.py,sha256=34HmvLo07j45Uvd2VR-2aRQ7lJD91sTK6zJgn5fphbQ,6755
|
|
||||||
_distutils_hack/override.py,sha256=Eu_s-NF6VIZ4Cqd0tbbA5wtWky2IZPNd8et6GLt1mzo,44
|
|
||||||
pkg_resources/__init__.py,sha256=-rh7XOnTxdGuC-_9FAyu5D6s8BL1UsBehxUtj7a-IVo,126203
|
|
||||||
pkg_resources/api_tests.txt,sha256=XEdvy4igHHrq2qNHNMHnlfO6XSQKNqOyLHbl6QcpfAI,12595
|
|
||||||
pkg_resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
pkg_resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
pkg_resources/tests/test_find_distributions.py,sha256=U91cov5L1COAIWLNq3Xy4plU7_MnOE1WtXMu6iV2waM,1972
|
|
||||||
pkg_resources/tests/test_integration_zope_interface.py,sha256=nzVoK557KZQN0V3DIQ1sVeaCOgt4Kpl-CODAWsO7pmc,1652
|
|
||||||
pkg_resources/tests/test_markers.py,sha256=0orKg7UMDf7fnuNQvRMOc-EF9EAP_JTQnk4mtGgbW50,241
|
|
||||||
pkg_resources/tests/test_pkg_resources.py,sha256=5Mt4bJQhLCL8j8cC46Uv32Np2Xc1TTxLGawIfET55Fk,17111
|
|
||||||
pkg_resources/tests/test_resources.py,sha256=K0LqMAUGpRQ9pUb9K0vyI7GesvtlQvTH074m-E2VQlo,31252
|
|
||||||
pkg_resources/tests/test_working_set.py,sha256=lRtGJWIixSwSMSbjHgRxeJEQiLMRXcz3xzJL2qL7eXY,8602
|
|
||||||
pkg_resources/tests/data/my-test-package-source/setup.cfg,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
pkg_resources/tests/data/my-test-package-source/setup.py,sha256=1VobhAZbMb7M9mfhb_NE8PwDsvukoWLs9aUAS0pYhe8,105
|
|
||||||
pkg_resources/tests/data/my-test-package-zip/my-test-package.zip,sha256=AYRcQ39GVePPnMT8TknP1gdDHyJnXhthESmpAjnzSCI,1809
|
|
||||||
pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/PKG-INFO,sha256=JvWv9Io2PAuYwEEw2fBW4Qc5YvdbkscpKX1kmLzsoHk,187
|
|
||||||
pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/SOURCES.txt,sha256=4ClkH8eTovZrdVrJFsVuxdbMEF--lBVSuKonDAPE5Jc,208
|
|
||||||
pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/dependency_links.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
||||||
pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
||||||
pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
||||||
pkg_resources/tests/data/my-test-package_zipped-egg/my_test_package-1.0-py3.7.egg,sha256=ZTlMGxjRGiKDNkiA2c75jbQH2TWIteP00irF9gvczbo,843
|
|
||||||
setuptools/__init__.py,sha256=AQsMeRFWz9CQ9aBtcSkbB4Dn8t2w86-GPriklpoAjew,10406
|
|
||||||
setuptools/_core_metadata.py,sha256=T7Tjp-WSoN881adev3R1wzXCPnkDHqbC2MgylN1yjS8,11978
|
|
||||||
setuptools/_entry_points.py,sha256=Y3QUE9JKFW_YyquDnpffNWSs6f3jKEt1e-dnx--9-Kw,2310
|
|
||||||
setuptools/_imp.py,sha256=YY1EjZEN-0zYci1cxO10B_adAEOr7i8eK8JoCc9Ierc,2435
|
|
||||||
setuptools/_importlib.py,sha256=aKIjcK0HKXNz2D-XTrxaixGn_juTkONwmu3dcheMOF0,223
|
|
||||||
setuptools/_itertools.py,sha256=jWRfsIrpC7myooz3hDURj9GtvpswZeKXg2HakmEhNjo,657
|
|
||||||
setuptools/_normalization.py,sha256=kAmGfrwjF5djydEfLLyKgjkXCbL_0_ZxUPO-DlLlmIY,5824
|
|
||||||
setuptools/_path.py,sha256=cPv41v03HD7uEYqCIo-E_cGRfpPVr4lywBCiK-HSrCg,2685
|
|
||||||
setuptools/_reqs.py,sha256=QI3C9uOBSNRccu208qPnixHx51nxCry7_nPTIJaSYxM,1438
|
|
||||||
setuptools/_shutil.py,sha256=cAOllcoyMTXs5JLoybQi29yI5gABk82hepJyOBv2bMw,1496
|
|
||||||
setuptools/_static.py,sha256=GTR79gESF1_JaK4trLkpDrEuCeEtPlwQW0MRv7VNQX4,4855
|
|
||||||
setuptools/archive_util.py,sha256=Tl_64hSTtc4y8x7xa98rFVUbG24oArpjzLAYGYP2_sI,7356
|
|
||||||
setuptools/build_meta.py,sha256=3cHAWucJaLA9DU5OfCbKkkteTDiQ5bB4LokfTRMgJT4,19968
|
|
||||||
setuptools/cli-32.exe,sha256=MqzBvFQxFsviz_EMuGd3LfLyVP8mNMhwrvC0bEtpb9s,11776
|
|
||||||
setuptools/cli-64.exe,sha256=u7PeVwdinmpgoMI4zUd7KPB_AGaYL9qVP6b87DkHOko,14336
|
|
||||||
setuptools/cli-arm64.exe,sha256=uafQjaiA36yLz1SOuksG-1m28JsX0zFIoPZhgyiSbGE,13824
|
|
||||||
setuptools/cli.exe,sha256=MqzBvFQxFsviz_EMuGd3LfLyVP8mNMhwrvC0bEtpb9s,11776
|
|
||||||
setuptools/depends.py,sha256=jKYfjmt_2ZQYVghb8L9bU7LJ6erHJ5ze-K_fKV1BMXk,5965
|
|
||||||
setuptools/discovery.py,sha256=-42c3XhwzkfodDKKP50C2YBzr11fncAgmUzBdBRb0-Q,21258
|
|
||||||
setuptools/dist.py,sha256=RZz7aj9RxqSYriqgFoZOu-7KIV82cmlaMFx9owZrApQ,44897
|
|
||||||
setuptools/errors.py,sha256=gY2x2PIaIgy01yRANRC-zcCwxDCqCScgJoCOZFe0yio,3024
|
|
||||||
setuptools/extension.py,sha256=KCnv9p3tgm0ZVqtgE451fyILsm4hCyvOiUtOu787D-4,6683
|
|
||||||
setuptools/glob.py,sha256=AC_B33DY8g-CHELxDsJrtwFrpiucSAZsakPFdSOQzhc,6062
|
|
||||||
setuptools/gui-32.exe,sha256=hdrh6V13hF8stZvKw9Sv50u-TJGpvMW_SnHNQxBNvnw,11776
|
|
||||||
setuptools/gui-64.exe,sha256=NHG2FA6txkEid9u-_j_vjDRaDxpZd2CGuAo2GMOoPjs,14336
|
|
||||||
setuptools/gui-arm64.exe,sha256=5pT0dDQFyLWSb_RX22_n8aEt7HwWqcOGR4TT9OB64Jc,13824
|
|
||||||
setuptools/gui.exe,sha256=hdrh6V13hF8stZvKw9Sv50u-TJGpvMW_SnHNQxBNvnw,11776
|
|
||||||
setuptools/installer.py,sha256=_4Wegx4r3L05sMo3-IlqFp-OuxnWyBqjyMZ7LWQXmh8,5110
|
|
||||||
setuptools/launch.py,sha256=IBb5lEv69CyuZ9ewIrmKlXh154kdLmP29LKfTMkximE,820
|
|
||||||
setuptools/logging.py,sha256=W16iHJ1HcCXYQ0RxyrEfJ83FT4175tCtoYg-E6uSpVI,1261
|
|
||||||
setuptools/modified.py,sha256=ZwbfBfCFP88ltvbv_dJDz-t1LsQjnM-JUpgZnnQZjjM,568
|
|
||||||
setuptools/monkey.py,sha256=FwMWl2n1v2bHbeqBy-o9g8yUNaAkYFbszCbXe9d5Za8,3717
|
|
||||||
setuptools/msvc.py,sha256=vmM0qL4rIzrtD9pia9ZEwtqZ4LbbrgL0dU0EANVYRm8,41631
|
|
||||||
setuptools/namespaces.py,sha256=2GGqYY1BNDEhMtBc1rHTv7klgmNVRdksJeW-L1f--ys,3171
|
|
||||||
setuptools/package_index.py,sha256=V5hnQtbDy1R6l3dSBNTuMUBvtrQxNPxgmYyXfpSq79U,40299
|
|
||||||
setuptools/sandbox.py,sha256=fMqtcOuipHO6RKPh1YB5o7d985dLKo76Whp3vrIei2E,14906
|
|
||||||
setuptools/script (dev).tmpl,sha256=RUzQzCQUaXtwdLtYHWYbIQmOaES5Brqq1FvUA_tu-5I,218
|
|
||||||
setuptools/script.tmpl,sha256=WGTt5piezO27c-Dbx6l5Q4T3Ff20A5z7872hv3aAhYY,138
|
|
||||||
setuptools/unicode_utils.py,sha256=ukMGh8pEAw6F_Ezb-K5D3c-078RgA_GcF0oW6lg4lSs,3189
|
|
||||||
setuptools/version.py,sha256=WJCeUuyq74Aok2TeK9-OexZOu8XrlQy7-y0BEuWNovQ,161
|
|
||||||
setuptools/warnings.py,sha256=oY0Se5eOqje_FEyjTgonUc0XGwgsrI5cgm1kkwulz_w,3796
|
|
||||||
setuptools/wheel.py,sha256=xkAtvgm7uPTyYV2zqVmQ0wA8kLwOyRT2Jes1zAy07Ks,8624
|
|
||||||
setuptools/windows_support.py,sha256=wW4IYLM1Bv7Z1MaauP2xmPjyy-wkmQnXdyvXscAf9fw,726
|
|
||||||
setuptools/_distutils/__init__.py,sha256=xGYuhWwLG07J0Q49BVnEjPy6wyDcd6veJMDJX7ljlyM,359
|
|
||||||
setuptools/_distutils/_log.py,sha256=i-lNTTcXS8TmWITJ6DODGvtW5z5tMattJQ76h8rZxQU,42
|
|
||||||
setuptools/_distutils/_macos_compat.py,sha256=JzUGhF4E5yIITHbUaPobZEWjGHdrrcNV63z86S4RjBc,239
|
|
||||||
setuptools/_distutils/_modified.py,sha256=RF1n1CexyDYV3lvGbeXS0s-XCJVboDOIUbA8wEQqYTY,3211
|
|
||||||
setuptools/_distutils/_msvccompiler.py,sha256=9PSfSHxvJnHnQL6Sqz4Xcz7iaBIT62p6BheQzGsSlwo,335
|
|
||||||
setuptools/_distutils/archive_util.py,sha256=Qw2z-Pt-NV8lNUQrzjs3XDGWCWHMPnqHLyt8TiD2XEA,8884
|
|
||||||
setuptools/_distutils/ccompiler.py,sha256=FKVjqzGJ7c-FtouNjhLiaMPm5LKMZHHAruXf8LU216c,524
|
|
||||||
setuptools/_distutils/cmd.py,sha256=hXtaRaH7QBnfNOIqEvCt47iwZzD9MVvBdhhdQctHsxM,22186
|
|
||||||
setuptools/_distutils/core.py,sha256=GEHKaFC48T3o-_SmH4864GvKyx1IgbVC6ISIPVlx7a4,9364
|
|
||||||
setuptools/_distutils/cygwinccompiler.py,sha256=mG_cU8SVZ4amD_VtF5vH6BXP0-kghGsDPbDSXrQ963c,594
|
|
||||||
setuptools/_distutils/debug.py,sha256=N6MrTAqK6l9SVk6tWweR108PM8Ol7qNlfyV-nHcLhsY,139
|
|
||||||
setuptools/_distutils/dep_util.py,sha256=xN75p6ZpHhMiHEc-rpL2XilJQynHnDNiafHteaZ4tjU,349
|
|
||||||
setuptools/_distutils/dir_util.py,sha256=DXPUlfVVGsg9B-Jgg4At_j9T7vM60OgwNXkQHqTo7-I,7236
|
|
||||||
setuptools/_distutils/dist.py,sha256=gW598UE0WMkzXQQ31Nr-8L7MPw0oIOz5OSSRzYZlwrM,55794
|
|
||||||
setuptools/_distutils/errors.py,sha256=PPE2oDRh5y9Q1beKK9rhdvDaCzQhi4HCXs4KcqfqgZY,3092
|
|
||||||
setuptools/_distutils/extension.py,sha256=Foyu4gULcPqm1_U9zrYYHxNk4NqglXv1rbsOk_QrSds,11155
|
|
||||||
setuptools/_distutils/fancy_getopt.py,sha256=PjdO-bWCW0imV_UN-MGEw9R2GP2OiE8pHjITgmTAY3Q,17895
|
|
||||||
setuptools/_distutils/file_util.py,sha256=YFQL_pD3hLuER9II_H6-hDC_YIGEookdd4wedLuiTW0,7978
|
|
||||||
setuptools/_distutils/filelist.py,sha256=MBeSRJmPcKmDv8ooZgSU4BiQPZ0Khwv8l_jhD50XycI,15337
|
|
||||||
setuptools/_distutils/log.py,sha256=VyBs5j7z4-K6XTEEBThUc9HyMpoPLGtQpERqbz5ylww,1200
|
|
||||||
setuptools/_distutils/spawn.py,sha256=zseCh9sEifyp0I5Vg719JNIASlROJ2ehXqQnHlpt89Q,4086
|
|
||||||
setuptools/_distutils/sysconfig.py,sha256=KeI8OHbMuEzHJ8Q0cBez9KZny8iRy6Z6Y0AkMz1jlsU,19728
|
|
||||||
setuptools/_distutils/text_file.py,sha256=z4dkOJBr9Bo2LG0TNqm8sD63LEEaKSSP0J0bWBrFG3c,12101
|
|
||||||
setuptools/_distutils/unixccompiler.py,sha256=1bXJWH4fiu_A2WfriHzf88xjllQTXnnjUkZdRKs9cWU,212
|
|
||||||
setuptools/_distutils/util.py,sha256=Njfnqk60zMdkiAjRnGcTWX3t49-obHapOlbNvyIl02I,18094
|
|
||||||
setuptools/_distutils/version.py,sha256=vImT5-ECXkQ21oKL0XYFiTqK6NyM09cpzBNoA_34CQU,12619
|
|
||||||
setuptools/_distutils/versionpredicate.py,sha256=qBWQ6wTj12ODytoTmIydefIY2jb4uY1sdbgbuLn-IJM,5205
|
|
||||||
setuptools/_distutils/zosccompiler.py,sha256=svdiXZ2kdcwKrJKfhUhib03y8gz7aGZKukXH3I7YkBc,58
|
|
||||||
setuptools/_distutils/command/__init__.py,sha256=GfFAzbBqk1qxSH4BdaKioKS4hRRnD44BAmwEN85C4u8,386
|
|
||||||
setuptools/_distutils/command/_framework_compat.py,sha256=0iZdSJYzGRWCCvzRDKE-R0-_yaAYvFMd1ylXb2eYXug,1609
|
|
||||||
setuptools/_distutils/command/bdist.py,sha256=jWtk61R7fWNUUNxJV0thTZzU5n80L3Ay1waSiP9kiLA,5854
|
|
||||||
setuptools/_distutils/command/bdist_dumb.py,sha256=Hx1jAqoZNxYIy4N5TLzUp6J5fi8Ls18py7UlLNFhO2E,4631
|
|
||||||
setuptools/_distutils/command/bdist_rpm.py,sha256=nxcXXv5a7B-1ntWu4DbGmCtES4EBINrJaBQcRNAYCJI,21785
|
|
||||||
setuptools/_distutils/command/build.py,sha256=SpHlagf0iNaKVyIhxDfhPFZ8X1-LAWOCQACy-yt2K0w,5923
|
|
||||||
setuptools/_distutils/command/build_clib.py,sha256=aMqZcUfCbOAu_xr-A9iW-Q9YZHzpDGLRTezOgMQJmSQ,7777
|
|
||||||
setuptools/_distutils/command/build_ext.py,sha256=zrrsu9HXnzV6bXYbJuZCK4SwVZMjKnl4pG1o3bNcxtc,32710
|
|
||||||
setuptools/_distutils/command/build_py.py,sha256=Vfq-INemoMbg6f003BTy_Ufp8bjOZhmFIhpKMcfXLgs,16696
|
|
||||||
setuptools/_distutils/command/build_scripts.py,sha256=tUpEzwTsnrP8qrNory3ldPB240QDCFkMp-pdYPs2wTk,5118
|
|
||||||
setuptools/_distutils/command/check.py,sha256=yoNe2MPY4JcTM7rwoIQdfZ75q5Ri058I2coi-Gq9CjM,4946
|
|
||||||
setuptools/_distutils/command/clean.py,sha256=dQAacOabwBXU9JoZ-1GFusq3eFltDaeXJFSYncqGbvE,2644
|
|
||||||
setuptools/_distutils/command/config.py,sha256=qrrfz6NEQORmbqiY2XlvCDWYhsbLyxZXJsURKfYN_kw,12724
|
|
||||||
setuptools/_distutils/command/install.py,sha256=-JenB-mua4hc2RI_-W8F9PnP_J-OaFO7E0PJGKxLo1o,30072
|
|
||||||
setuptools/_distutils/command/install_data.py,sha256=GzBlUWWKubTYJlP-L0auUriq9cL-5RKOcoyHTttKj0Q,2875
|
|
||||||
setuptools/_distutils/command/install_egg_info.py,sha256=ffiLoU1ivQJ8q2_WL7ZygZbUcOsgdFLKL7otEIJWWkI,2868
|
|
||||||
setuptools/_distutils/command/install_headers.py,sha256=5ciKCj8c3XKsYNKdkdMvnypaUCKcoWCDeeZij3fD-Z4,1272
|
|
||||||
setuptools/_distutils/command/install_lib.py,sha256=2s9-m5-b1qKm51F28lB5L39Z6vv_GHMlv9dNBSupok0,8588
|
|
||||||
setuptools/_distutils/command/install_scripts.py,sha256=M0pPdiaqB7TGmqTMujpGGeiL0Iq_CTeGjMFtrmDmwzM,2002
|
|
||||||
setuptools/_distutils/command/sdist.py,sha256=cRIF6Ht1hJ6ayOOFVycMFBUNxjo94e_rFYPx4Hi8Ahc,19151
|
|
||||||
setuptools/_distutils/compat/__init__.py,sha256=J20aXGjJ86Rg41xFLIWlcWCgZ9edMdJ9vvdNEQ87vPQ,522
|
|
||||||
setuptools/_distutils/compat/numpy.py,sha256=UFgneZw9w97g4c-yGoAIOyLxUOWQ-fPRIhhfMs7_Ouc,167
|
|
||||||
setuptools/_distutils/compat/py39.py,sha256=hOsD6lwZLqZoMnacNJ3P6nUA-LJQhEpVtYTzVH0o96M,1964
|
|
||||||
setuptools/_distutils/compilers/C/base.py,sha256=XR1rBCStCquqm7QOYXD41-LfvsFcPpGxrwxeXzJyn_w,54876
|
|
||||||
setuptools/_distutils/compilers/C/cygwin.py,sha256=DUlwQSb55aj7OdcmcddrmCmVEjEaxIiJ5hHUO3GBPNs,11844
|
|
||||||
setuptools/_distutils/compilers/C/errors.py,sha256=sKOVzJajMUmNdfywo9UM_QQGsKFcclDhtI5TlCiXMLc,573
|
|
||||||
setuptools/_distutils/compilers/C/msvc.py,sha256=elzG8v9jN5QytLMwLCdUdSuZ3eZ3R98VUvnm9Y2PBCA,21404
|
|
||||||
setuptools/_distutils/compilers/C/unix.py,sha256=YH-y9g_pjBFjaJyHJQkDEBQ7q4D20I2-cWJNdgw-Yho,16531
|
|
||||||
setuptools/_distutils/compilers/C/zos.py,sha256=vnNeWLRZkdIkdZ-YyBnL8idTUfcCOn0tLMW5OBJ0ScU,6586
|
|
||||||
setuptools/_distutils/compilers/C/tests/test_base.py,sha256=rdhHc56bhXtm5NnN9BSHwr6c69UqzMItZQzlw2AsdMc,2706
|
|
||||||
setuptools/_distutils/compilers/C/tests/test_cygwin.py,sha256=UgV2VgUXj3VulcbDc0UBWfEyJDx42tgSwS4LzHix3mY,2701
|
|
||||||
setuptools/_distutils/compilers/C/tests/test_mingw.py,sha256=hCmwyywISpRoyOySbFHBL4TprWRV0mUWDKmOLO8XBXE,1900
|
|
||||||
setuptools/_distutils/compilers/C/tests/test_msvc.py,sha256=DlGjmZ1mBSMXIgmlu80BKc7V-EJOZuYucwJwFh5dn28,4151
|
|
||||||
setuptools/_distutils/compilers/C/tests/test_unix.py,sha256=AyadWw1fR-UeDl2TvIbYBzOJVHkpE_oRRQ3JTJWqaEA,14642
|
|
||||||
setuptools/_distutils/tests/__init__.py,sha256=j-IoPZEtQv3EOPuqNTwalr6GLyRjzCC-OOaNvZzmHsI,1485
|
|
||||||
setuptools/_distutils/tests/support.py,sha256=tjsYsyxvpTK4NrkCseh2ujvDIGV0Mf_b5SI5fP2T0yM,4099
|
|
||||||
setuptools/_distutils/tests/test_archive_util.py,sha256=jozimSwPBF-JoJfN_vDaiVGZp66BNcWZGh34FlW57DQ,11787
|
|
||||||
setuptools/_distutils/tests/test_bdist.py,sha256=xNHxUsLlHsZQRwkzLb_iSD24s-9Mk-NX2ffBWwOyPyc,1396
|
|
||||||
setuptools/_distutils/tests/test_bdist_dumb.py,sha256=QF05MHNhPOdZyh88Xpw8KsO64s7pRFkl8KL-RoV4XK0,2247
|
|
||||||
setuptools/_distutils/tests/test_bdist_rpm.py,sha256=Hdm-pwWgyaoGdGbEcGZa8cRhGU45y8gHK8umOanTjik,3932
|
|
||||||
setuptools/_distutils/tests/test_build.py,sha256=JJY5XpOZco25ZY0pstxl-iI8mHsWP0ujf5o8aOtuZYY,1742
|
|
||||||
setuptools/_distutils/tests/test_build_clib.py,sha256=Mo1ZFb4C1VXBYOGvnallwN7YCnTtr24akLDO8Zi4CsY,4331
|
|
||||||
setuptools/_distutils/tests/test_build_ext.py,sha256=QFO9qYVhWWdJu17HXc4x9RMnLZlhk0lAHi9HVppbuX4,22545
|
|
||||||
setuptools/_distutils/tests/test_build_py.py,sha256=NsfmRrojOHBXNMqWR_mp5g4PLTgjhD7iZFUffGZFIdw,6882
|
|
||||||
setuptools/_distutils/tests/test_build_scripts.py,sha256=cD-FRy-oX55sXRX5Ez5xQCaeHrWajyKc4Xuwv2fe48w,2880
|
|
||||||
setuptools/_distutils/tests/test_check.py,sha256=hHSV07qf7YoSxGsTbbsUQ9tssZz5RRNdbrY1s2SwaFI,6226
|
|
||||||
setuptools/_distutils/tests/test_clean.py,sha256=hPH6jfIpGFUrvWbF1txkiNVSNaAxt2wq5XjV499zO4E,1240
|
|
||||||
setuptools/_distutils/tests/test_cmd.py,sha256=bgRB79mitoOKR1OiyZHnCogvGxt3pWkxeTqIC04lQWQ,3254
|
|
||||||
setuptools/_distutils/tests/test_config_cmd.py,sha256=Zs6WX0IfxDvmuC19XzuVNnYCnTr9Y-hl73TAmDSBN4Y,2664
|
|
||||||
setuptools/_distutils/tests/test_core.py,sha256=L7XKVAxa-MGoAZeANopnuK9fRKneYhkSQpgw8XQvcF8,3829
|
|
||||||
setuptools/_distutils/tests/test_dir_util.py,sha256=E84lC-k4riVUwURyWaQ0Jqx2ui2-io-0RuJa3M7qkJs,4500
|
|
||||||
setuptools/_distutils/tests/test_dist.py,sha256=a6wlc5fQJd5qQ6HOndzcupNhjTxvj6-_JLtpuYvaP1M,18793
|
|
||||||
setuptools/_distutils/tests/test_extension.py,sha256=-YejLgZCuycFrOBd64pVH0JvwMc9NwhzHvQxvvjXHqk,3670
|
|
||||||
setuptools/_distutils/tests/test_file_util.py,sha256=livjnl3FkilQlrB2rFdFQq9nvjEVZHynNya0bfzv_b4,3522
|
|
||||||
setuptools/_distutils/tests/test_filelist.py,sha256=rJwkqCUfkGDgWlD22TozsT8ycbupMHB8DXqThzwT1T4,10766
|
|
||||||
setuptools/_distutils/tests/test_install.py,sha256=TfCB0ykhIxydIC2Q4SuTAZzSHvteMHgrBL9whoSgK9Q,8618
|
|
||||||
setuptools/_distutils/tests/test_install_data.py,sha256=vKq3K97k0hBAnOg38nmwEdf7cEDVr9rTVyCeJolgb4A,2464
|
|
||||||
setuptools/_distutils/tests/test_install_headers.py,sha256=PVAYpo_tYl980Qf64DPOmmSvyefIHdU06f7VsJeZykE,936
|
|
||||||
setuptools/_distutils/tests/test_install_lib.py,sha256=qri6Rl-maNTQrNDV8DbeXNl0hjsfRIKiI4rfZLrmWBI,3612
|
|
||||||
setuptools/_distutils/tests/test_install_scripts.py,sha256=KE3v0cDkFW-90IOID-OmZZGM2mhy-ZkEuuW7UXS2SHw,1600
|
|
||||||
setuptools/_distutils/tests/test_log.py,sha256=isFtOufloCyEdZaQOV7cVUr46GwtdVMj43mGBB5XH7k,323
|
|
||||||
setuptools/_distutils/tests/test_modified.py,sha256=h1--bOWmtJo1bpVV6uRhdnS9br71CBiNDM1MDwSGpug,4221
|
|
||||||
setuptools/_distutils/tests/test_sdist.py,sha256=cfzUhlCA418-1vH9ta3IBs26c_jUBbkJoFOK5GnAyNk,15062
|
|
||||||
setuptools/_distutils/tests/test_spawn.py,sha256=eS8w9D7bTxyFLSyRahJWeuh8Kc1F8RWWiY_dSG5B5Bc,4803
|
|
||||||
setuptools/_distutils/tests/test_sysconfig.py,sha256=lxM8LsUi1TomjDV4HoYK8u5nUoBkeNL60Uq8PY1DcwU,11986
|
|
||||||
setuptools/_distutils/tests/test_text_file.py,sha256=WQWSB5AfdBDZaMA8BFgipJPnsJb_2SKMfL90fSkRVtw,3460
|
|
||||||
setuptools/_distutils/tests/test_util.py,sha256=H9zlZ4z4Vh4TfjNYDBsxP7wguQLpxCfJYyOcm1yZU3c,7988
|
|
||||||
setuptools/_distutils/tests/test_version.py,sha256=ahfg_mP8wRy1sgwY-_Px5hrjgf6_upTIpnCgpR4yWRk,2750
|
|
||||||
setuptools/_distutils/tests/test_versionpredicate.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_distutils/tests/unix_compat.py,sha256=z-op6C2iVdX1aq5BIBR7cqOxijKE97alNwJqHNdLpoI,386
|
|
||||||
setuptools/_distutils/tests/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_distutils/tests/compat/py39.py,sha256=t0GBTM-30jX-9zCfkwlNBFtzzabemx6065mJ0d9_VRw,1026
|
|
||||||
setuptools/_vendor/typing_extensions.py,sha256=gwekpyG9DVG3lxWKX4ni8u7nk3We5slG98mA9F3DJQw,134451
|
|
||||||
setuptools/_vendor/autocommand/__init__.py,sha256=zko5Rnvolvb-UXjCx_2ArPTGBWwUK5QY4LIQIKYR7As,1037
|
|
||||||
setuptools/_vendor/autocommand/autoasync.py,sha256=AMdyrxNS4pqWJfP_xuoOcImOHWD-qT7x06wmKN1Vp-U,5680
|
|
||||||
setuptools/_vendor/autocommand/autocommand.py,sha256=hmkEmQ72HtL55gnURVjDOnsfYlGd5lLXbvT4KG496Qw,2505
|
|
||||||
setuptools/_vendor/autocommand/automain.py,sha256=A2b8i754Mxc_DjU9WFr6vqYDWlhz0cn8miu8d8EsxV8,2076
|
|
||||||
setuptools/_vendor/autocommand/autoparse.py,sha256=WVWmZJPcbzUKXP40raQw_0HD8qPJ2V9VG1eFFmmnFxw,11642
|
|
||||||
setuptools/_vendor/autocommand/errors.py,sha256=7aa3roh9Herd6nIKpQHNWEslWE8oq7GiHYVUuRqORnA,886
|
|
||||||
setuptools/_vendor/backports/__init__.py,sha256=iOEMwnlORWezdO8-2vxBIPSR37D7JGjluZ8f55vzxls,81
|
|
||||||
setuptools/_vendor/backports/tarfile/__init__.py,sha256=Pwf2qUIfB0SolJPCKcx3vz3UEu_aids4g4sAfxy94qg,108491
|
|
||||||
setuptools/_vendor/backports/tarfile/__main__.py,sha256=Yw2oGT1afrz2eBskzdPYL8ReB_3liApmhFkN2EbDmc4,59
|
|
||||||
setuptools/_vendor/backports/tarfile/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/backports/tarfile/compat/py38.py,sha256=iYkyt_gvWjLzGUTJD9TuTfMMjOk-ersXZmRlvQYN2qE,568
|
|
||||||
setuptools/_vendor/importlib_metadata/__init__.py,sha256=tZNB-23h8Bixi9uCrQqj9Yf0aeC--Josdy3IZRIQeB0,33798
|
|
||||||
setuptools/_vendor/importlib_metadata/_adapters.py,sha256=rIhWTwBvYA1bV7i-5FfVX38qEXDTXFeS5cb5xJtP3ks,2317
|
|
||||||
setuptools/_vendor/importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
|
|
||||||
setuptools/_vendor/importlib_metadata/_compat.py,sha256=73QKrN9KNoaZzhbX5yPCCZa-FaALwXe8TPlDR72JgBU,1314
|
|
||||||
setuptools/_vendor/importlib_metadata/_functools.py,sha256=PsY2-4rrKX4RVeRC1oGp1lB1pmC9eKN88_f-bD9uOoA,2895
|
|
||||||
setuptools/_vendor/importlib_metadata/_itertools.py,sha256=cvr_2v8BRbxcIl5x5ldfqdHjhI8Yi8s8yk50G_nm6jQ,2068
|
|
||||||
setuptools/_vendor/importlib_metadata/_meta.py,sha256=nxZ7C8GVlcBFAKWyVOn_dn7ot_twBcbm1NmvjIetBHI,1801
|
|
||||||
setuptools/_vendor/importlib_metadata/_text.py,sha256=HCsFksZpJLeTP3NEk_ngrAeXVRRtTrtyh9eOABoRP4A,2166
|
|
||||||
setuptools/_vendor/importlib_metadata/diagnose.py,sha256=nkSRMiowlmkhLYhKhvCg9glmt_11Cox-EmLzEbqYTa8,379
|
|
||||||
setuptools/_vendor/importlib_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/importlib_metadata/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/importlib_metadata/compat/py311.py,sha256=uqm-K-uohyj1042TH4a9Er_I5o7667DvulcD-gC_fSA,608
|
|
||||||
setuptools/_vendor/importlib_metadata/compat/py39.py,sha256=cPkMv6-0ilK-0Jw_Tkn0xYbOKJZc4WJKQHow0c2T44w,1102
|
|
||||||
setuptools/_vendor/inflect/__init__.py,sha256=Jxy1HJXZiZ85kHeLAhkmvz6EMTdFqBe-duvt34R6IOc,103796
|
|
||||||
setuptools/_vendor/inflect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/inflect/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/inflect/compat/py38.py,sha256=oObVfVnWX9_OpnOuEJn1mFbJxVhwyR5epbiTNXDDaso,160
|
|
||||||
setuptools/_vendor/jaraco/context.py,sha256=REoLIxDkO5MfEYowt_WoupNCRoxBS5v7YX2PbW8lIcs,9552
|
|
||||||
setuptools/_vendor/jaraco/collections/__init__.py,sha256=Pc1-SqjWm81ad1P0-GttpkwO_LWlnaY6gUq8gcKh2v0,26640
|
|
||||||
setuptools/_vendor/jaraco/collections/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/jaraco/functools/__init__.py,sha256=hEAJaS2uSZRuF_JY4CxCHIYh79ZpxaPp9OiHyr9EJ1w,16642
|
|
||||||
setuptools/_vendor/jaraco/functools/__init__.pyi,sha256=gk3dsgHzo5F_U74HzAvpNivFAPCkPJ1b2-yCd62dfnw,3878
|
|
||||||
setuptools/_vendor/jaraco/functools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/jaraco/text/Lorem ipsum.txt,sha256=N_7c_79zxOufBY9HZ3yzMgOkNv-TkOTTio4BydrSjgs,1335
|
|
||||||
setuptools/_vendor/jaraco/text/__init__.py,sha256=Y2YUqXR_orUoDaY4SkPRe6ZZhb5HUHB_Ah9RCNsVyho,16250
|
|
||||||
setuptools/_vendor/jaraco/text/layouts.py,sha256=HTC8aSTLZ7uXipyOXapRMC158juecjK6RVwitfmZ9_w,643
|
|
||||||
setuptools/_vendor/jaraco/text/show-newlines.py,sha256=WGQa65e8lyhb92LUOLqVn6KaCtoeVgVws6WtSRmLk6w,904
|
|
||||||
setuptools/_vendor/jaraco/text/strip-prefix.py,sha256=NfVXV8JVNo6nqcuYASfMV7_y4Eo8zMQqlCOGvAnRIVw,412
|
|
||||||
setuptools/_vendor/jaraco/text/to-dvorak.py,sha256=1SNcbSsvISpXXg-LnybIHHY-RUFOQr36zcHkY1pWFqw,119
|
|
||||||
setuptools/_vendor/jaraco/text/to-qwerty.py,sha256=s4UMQUnPwFn_dB5uZC27BurHOQcYondBfzIpVL5pEzw,119
|
|
||||||
setuptools/_vendor/more_itertools/__init__.py,sha256=dtAbGjTDmn_ghiU5YXfhyDy0phAlXVdt5klZA5fUa-Q,149
|
|
||||||
setuptools/_vendor/more_itertools/__init__.pyi,sha256=5B3eTzON1BBuOLob1vCflyEb2lSd6usXQQ-Cv-hXkeA,43
|
|
||||||
setuptools/_vendor/more_itertools/more.py,sha256=1E5kzFncRKTDw0cYv1yRXMgDdunstLQd1QStcnL6U90,148370
|
|
||||||
setuptools/_vendor/more_itertools/more.pyi,sha256=iXXeqt48Nxe8VGmIWpkVXuKpR2FYNuu2DU8nQLWCCu0,21484
|
|
||||||
setuptools/_vendor/more_itertools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/more_itertools/recipes.py,sha256=WedhhfhGVgr6zii8fIbGJVmRTw0ZKRiLKnYBDGJv4nY,28591
|
|
||||||
setuptools/_vendor/more_itertools/recipes.pyi,sha256=T_mdGpcFdfrP3JSWbwzYP9JyNV-Go-7RPfpxfftAWlA,4617
|
|
||||||
setuptools/_vendor/packaging/__init__.py,sha256=dk4Ta_vmdVJxYHDcfyhvQNw8V3PgSBomKNXqg-D2JDY,494
|
|
||||||
setuptools/_vendor/packaging/_elffile.py,sha256=cflAQAkE25tzhYmq_aCi72QfbT_tn891tPzfpbeHOwE,3306
|
|
||||||
setuptools/_vendor/packaging/_manylinux.py,sha256=vl5OCoz4kx80H5rwXKeXWjl9WNISGmr4ZgTpTP9lU9c,9612
|
|
||||||
setuptools/_vendor/packaging/_musllinux.py,sha256=p9ZqNYiOItGee8KcZFeHF_YcdhVwGHdK6r-8lgixvGQ,2694
|
|
||||||
setuptools/_vendor/packaging/_parser.py,sha256=s_TvTvDNK0NrM2QB3VKThdWFM4Nc0P6JnkObkl3MjpM,10236
|
|
||||||
setuptools/_vendor/packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
|
|
||||||
setuptools/_vendor/packaging/_tokenizer.py,sha256=J6v5H7Jzvb-g81xp_2QACKwO7LxHQA6ikryMU7zXwN8,5273
|
|
||||||
setuptools/_vendor/packaging/markers.py,sha256=c89TNzB7ZdGYhkovm6PYmqGyHxXlYVaLW591PHUNKD8,10561
|
|
||||||
setuptools/_vendor/packaging/metadata.py,sha256=YJibM7GYe4re8-0a3OlXmGS-XDgTEoO4tlBt2q25Bng,34762
|
|
||||||
setuptools/_vendor/packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/packaging/requirements.py,sha256=gYyRSAdbrIyKDY66ugIDUQjRMvxkH2ALioTmX3tnL6o,2947
|
|
||||||
setuptools/_vendor/packaging/specifiers.py,sha256=GG1wPNMcL0fMJO68vF53wKMdwnfehDcaI-r9NpTfilA,40074
|
|
||||||
setuptools/_vendor/packaging/tags.py,sha256=CFqrJzAzc2XNGexerH__T-Y5Iwq7WbsYXsiLERLWxY0,21014
|
|
||||||
setuptools/_vendor/packaging/utils.py,sha256=0F3Hh9OFuRgrhTgGZUl5K22Fv1YP2tZl1z_2gO6kJiA,5050
|
|
||||||
setuptools/_vendor/packaging/version.py,sha256=olfyuk_DPbflNkJ4wBWetXQ17c74x3DB501degUv7DY,16676
|
|
||||||
setuptools/_vendor/packaging/licenses/__init__.py,sha256=1x5M1nEYjcgwEbLt0dXwz2ukjr18DiCzC0sraQqJ-Ww,5715
|
|
||||||
setuptools/_vendor/packaging/licenses/_spdx.py,sha256=oAm1ztPFwlsmCKe7lAAsv_OIOfS1cWDu9bNBkeu-2ns,48398
|
|
||||||
setuptools/_vendor/platformdirs/__init__.py,sha256=EMGE8qeHRR9CzDFr8kL3tA8hdZZniYjXBVZd0UGTWK0,22225
|
|
||||||
setuptools/_vendor/platformdirs/__main__.py,sha256=HnsUQHpiBaiTxwcmwVw-nFaPdVNZtQIdi1eWDtI-MzI,1493
|
|
||||||
setuptools/_vendor/platformdirs/android.py,sha256=xZXY9Jd46WOsxT2U6-5HsNtDZ-IQqxcEUrBLl3hYk4o,9016
|
|
||||||
setuptools/_vendor/platformdirs/api.py,sha256=QBYdUac2eC521ek_y53uD1Dcq-lJX8IgSRVd4InC6uc,8996
|
|
||||||
setuptools/_vendor/platformdirs/macos.py,sha256=wftsbsvq6nZ0WORXSiCrZNkRHz_WKuktl0a6mC7MFkI,5580
|
|
||||||
setuptools/_vendor/platformdirs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/platformdirs/unix.py,sha256=Cci9Wqt35dAMsg6HT9nRGHSBW5obb0pR3AE1JJnsCXg,10643
|
|
||||||
setuptools/_vendor/platformdirs/version.py,sha256=r7F76tZRjgQKzrpx_I0_ZMQOMU-PS7eGnHD7zEK3KB0,411
|
|
||||||
setuptools/_vendor/platformdirs/windows.py,sha256=IFpiohUBwxPtCzlyKwNtxyW4Jk8haa6W8o59mfrDXVo,10125
|
|
||||||
setuptools/_vendor/platformdirs-4.2.2.dist-info/licenses/LICENSE,sha256=KeD9YukphQ6G6yjD_czwzv30-pSHkBHP-z0NS-1tTbY,1089
|
|
||||||
setuptools/_vendor/tomli/__init__.py,sha256=JhUwV66DB1g4Hvt1UQCVMdfCu-IgAV8FXmvDU9onxd4,396
|
|
||||||
setuptools/_vendor/tomli/_parser.py,sha256=g9-ENaALS-B8dokYpCuzUFalWlog7T-SIYMjLZSWrtM,22633
|
|
||||||
setuptools/_vendor/tomli/_re.py,sha256=dbjg5ChZT23Ka9z9DHOXfdtSpPwUfdgMXnj8NOoly-w,2943
|
|
||||||
setuptools/_vendor/tomli/_types.py,sha256=-GTG2VUqkpxwMqzmVO4F7ybKddIbAnuAHXfmWQcTi3Q,254
|
|
||||||
setuptools/_vendor/tomli/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
|
|
||||||
setuptools/_vendor/typeguard/__init__.py,sha256=Onh4w38elPCjtlcU3JY9k3h70NjsxXIkAflmQn-Z0FY,2071
|
|
||||||
setuptools/_vendor/typeguard/_checkers.py,sha256=JRrgKicdOEfIBoNEtegYCEIlhpad-a1u1Em7GCj0WCI,31360
|
|
||||||
setuptools/_vendor/typeguard/_config.py,sha256=nIz8QwDa-oFO3L9O8_6srzlmd99pSby2wOM4Wb7F_B0,2846
|
|
||||||
setuptools/_vendor/typeguard/_decorators.py,sha256=v6dsIeWvPhExGLP_wXF-RmDUyjZf_Ak28g7gBJ_v0-0,9033
|
|
||||||
setuptools/_vendor/typeguard/_exceptions.py,sha256=ZIPeiV-FBd5Emw2EaWd2Fvlsrwi4ocwT2fVGBIAtHcQ,1121
|
|
||||||
setuptools/_vendor/typeguard/_functions.py,sha256=ibgSAKa5ptIm1eR9ARG0BSozAFJPFNASZqhPVyQeqig,10393
|
|
||||||
setuptools/_vendor/typeguard/_importhook.py,sha256=ugjCDvFcdWMU7UugqlJG91IpVNpEIxtRr-99s0h1k7M,6389
|
|
||||||
setuptools/_vendor/typeguard/_memo.py,sha256=1juQV_vxnD2JYKbSrebiQuj4oKHz6n67v9pYA-CCISg,1303
|
|
||||||
setuptools/_vendor/typeguard/_pytest_plugin.py,sha256=-fcSqkv54rIfIF8pDavY5YQPkj4OX8GMt_lL7CQSD4I,4416
|
|
||||||
setuptools/_vendor/typeguard/_suppression.py,sha256=VQfzxcwIbu3if0f7VBkKM7hkYOA7tNFw9a7jMBsmMg4,2266
|
|
||||||
setuptools/_vendor/typeguard/_transformer.py,sha256=9Ha7_QhdwoUni_6hvdY-hZbuEergowHrNL2vzHIakFY,44937
|
|
||||||
setuptools/_vendor/typeguard/_union_transformer.py,sha256=v_42r7-6HuRX2SoFwnyJ-E5PlxXpVeUJPJR1-HU9qSo,1354
|
|
||||||
setuptools/_vendor/typeguard/_utils.py,sha256=5HhO1rPn5f1M6ymkVAEv7Xmlz1cX-j0OnTMlyHqqrR8,5270
|
|
||||||
setuptools/_vendor/typeguard/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/wheel/__init__.py,sha256=mrxMnvdXACur_LWegbUfh5g5ysWZrd63UJn890wvGNk,59
|
|
||||||
setuptools/_vendor/wheel/__main__.py,sha256=NkMUnuTCGcOkgY0IBLgBCVC_BGGcWORx2K8jYGS12UE,455
|
|
||||||
setuptools/_vendor/wheel/_bdist_wheel.py,sha256=UghCQjSH_pVfcZh6oRjzSw_TQhcf3anSx1OkiLSL82M,21694
|
|
||||||
setuptools/_vendor/wheel/_setuptools_logging.py,sha256=-5KC-lne0ilOUWIDfOkqapUWGMFZhuKYDIavIZiB5kM,781
|
|
||||||
setuptools/_vendor/wheel/bdist_wheel.py,sha256=tpf9WufiSO1RuEMg5oPhIfSG8DMziCZ_4muCKF69Cqo,1107
|
|
||||||
setuptools/_vendor/wheel/macosx_libfile.py,sha256=k1x7CE3LPtOVGqj6NXQ1nTGYVPaeRrhVzUG_KPq3zDs,16572
|
|
||||||
setuptools/_vendor/wheel/metadata.py,sha256=JC4p7jlQZu2bUTAQ2fevkqLjg_X6gnNyRhLn6OUO1tc,6171
|
|
||||||
setuptools/_vendor/wheel/util.py,sha256=aL7aibHwYUgfc8WlolL5tXdkV4DatbJxZHb1kwHFJAU,423
|
|
||||||
setuptools/_vendor/wheel/wheelfile.py,sha256=USCttNlJwafxt51YYFFKG7jnxz8dfhbyqAZL6jMTA9s,8411
|
|
||||||
setuptools/_vendor/wheel/cli/__init__.py,sha256=Npq6_jKi03dhIcRnmbuFhwviVJxwO0tYEnEhWMv9cJo,4402
|
|
||||||
setuptools/_vendor/wheel/cli/convert.py,sha256=Bi0ntEXb9nTllCxWeTRQ4j-nPs3szWSEKipG_GgnMkQ,12634
|
|
||||||
setuptools/_vendor/wheel/cli/pack.py,sha256=CAFcHdBVulvsHYJlndKVO7KMI9JqBTZz5ii0PKxxCOs,3103
|
|
||||||
setuptools/_vendor/wheel/cli/tags.py,sha256=lHw-LaWrkS5Jy_qWcw-6pSjeNM6yAjDnqKI3E5JTTCU,4760
|
|
||||||
setuptools/_vendor/wheel/cli/unpack.py,sha256=Y_J7ynxPSoFFTT7H0fMgbBlVErwyDGcObgme5MBuz58,1021
|
|
||||||
setuptools/_vendor/wheel/vendored/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/wheel/vendored/vendor.txt,sha256=Z2ENjB1i5prfez8CdM1Sdr3c6Zxv2rRRolMpLmBncAE,16
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/_manylinux.py,sha256=P7sdR5_7XBY09LVYYPhHmydMJIIwPXWsh4olk74Uuj4,9588
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/_musllinux.py,sha256=z1s8To2hQ0vpn_d-O2i5qxGwEK8WmGlLt3d_26V7NeY,2674
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/_parser.py,sha256=4tT4emSl2qTaU7VTQE1Xa9o1jMPCsBezsYBxyNMUN-s,10347
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/markers.py,sha256=_TSPI1BhJYO7Bp9AzTmHQxIqHEVXaTjmDh9G-w8qzPA,8232
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/requirements.py,sha256=dgoBeVprPu2YE6Q8nGfwOPTjATHbRa_ZGLyXhFEln6Q,2933
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/specifiers.py,sha256=IWSt0SrLSP72heWhAC8UL0eGvas7XIQHjqiViVfmPKE,39778
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/tags.py,sha256=fedHXiOHkBxNZTXotXv8uXPmMFU9ae-TKBujgYHigcA,18950
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/utils.py,sha256=XgdmP3yx9-wQEFjO7OvMj9RjEf5JlR5HFFR69v7SQ9E,5268
|
|
||||||
setuptools/_vendor/wheel/vendored/packaging/version.py,sha256=PFJaYZDxBgyxkfYhH3SQw4qfE9ICCWrTmitvq14y3bs,16234
|
|
||||||
setuptools/_vendor/zipp/__init__.py,sha256=QuI1g00G4fRAcGt-HqbV0oWIkmSgedCGGYsHHYzNa8A,13412
|
|
||||||
setuptools/_vendor/zipp/glob.py,sha256=etWpnfEoRyfUvrUsi6sTiGmErvPwe6HzY6pT8jg_lUI,3082
|
|
||||||
setuptools/_vendor/zipp/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/zipp/compat/py310.py,sha256=eZpkW0zRtunkhEh8jjX3gCGe22emoKCBJw72Zt4RkhA,219
|
|
||||||
setuptools/command/__init__.py,sha256=wdSrlNR0P6nCz9_oFtCAiAkeFJMsZa1jPcpXT53f0SM,803
|
|
||||||
setuptools/command/_requirestxt.py,sha256=ItYMTJGh_i5TlQstX_nFopqEhkC4PJFadBL2Zd3V670,4228
|
|
||||||
setuptools/command/alias.py,sha256=rDdrMt32DS6qf3K7tjZZyHD_dMKrm77AXcAtx-nBQ0I,2380
|
|
||||||
setuptools/command/bdist_egg.py,sha256=3eDucQ4fdeYMsLO9PhBfY1JkcMLhZXgnAI_9FdFNsEE,16972
|
|
||||||
setuptools/command/bdist_rpm.py,sha256=LyqI49w48SKk0FmuHsE9MLzX1SuXjL7YMNbZMFZqFII,1435
|
|
||||||
setuptools/command/bdist_wheel.py,sha256=_LfGHB7CV_uyncqlOTSETEi2gjVCoPKuAXcyRFoq7Cs,22246
|
|
||||||
setuptools/command/build.py,sha256=eI7STMERGGZEpzk1tvJN8p9IOjAAXMcGLzljv2mwI3M,6052
|
|
||||||
setuptools/command/build_clib.py,sha256=AbgpPIF_3qL8fZr3JIebI-WHTMTBiMfrFkVQz8K40G4,4528
|
|
||||||
setuptools/command/build_ext.py,sha256=bUH4M0NizaJJrv10wK-ZD3uY0TxCSZlYQDhiwwzHslM,18377
|
|
||||||
setuptools/command/build_py.py,sha256=DCbjvB18kkL-xUK5rvlzm0C6twTeOxNhyvJDxxa7fII,15539
|
|
||||||
setuptools/command/develop.py,sha256=zX22119sI1G1gfJ1gNCE4hkg2zbLKx0uUwvNmC5bIu8,6886
|
|
||||||
setuptools/command/dist_info.py,sha256=HU752iLLmmYMHbsDBgz2ubRjkgJobugOp8H71LzzDys,3450
|
|
||||||
setuptools/command/easy_install.py,sha256=0Z4kFlE78Ae2AfHXYTxZFUa2t3qlNqXCtQu4J1Nx8iA,87870
|
|
||||||
setuptools/command/editable_wheel.py,sha256=3bBipRZA3E4poQ8LEWhMYEjHjCzqU_pIdO86C3t7oNY,35624
|
|
||||||
setuptools/command/egg_info.py,sha256=WWUozR3DZCrWsTQhHXOygMiEUcrjLWphET0-Zsocsm4,25982
|
|
||||||
setuptools/command/install.py,sha256=MmTGb8m1R8fJ0cc5FTWCF6uq-s1ZJmvpYm-N_CtqEaI,7046
|
|
||||||
setuptools/command/install_egg_info.py,sha256=3I9IPCH7D59Sh-6aVYz-h6wwyxq-wkxrKwKg3nDdJqs,2075
|
|
||||||
setuptools/command/install_lib.py,sha256=9n1_U83eHcERL_a_rv_LhHCkhXlLdqyZ4SdBow-9qcE,4319
|
|
||||||
setuptools/command/install_scripts.py,sha256=tVOCj3e8OTIrkoL_bGbT5pOksdxZfQblH_bdI4DtVV4,2637
|
|
||||||
setuptools/command/launcher manifest.xml,sha256=xlLbjWrB01tKC0-hlVkOKkiSPbzMml2eOPtJ_ucCnbE,628
|
|
||||||
setuptools/command/rotate.py,sha256=XNd_BEEOWAJHW1FcLTMUWWl4QB6zAuk7b8VWQg3FHos,2187
|
|
||||||
setuptools/command/saveopts.py,sha256=Np0PVb7SD7oTbu9Z9sosS7D-CkkIkU7x4glu5Es1tjA,692
|
|
||||||
setuptools/command/sdist.py,sha256=JaQm2-ebXI2kvyrBjJKP8yNLPa5eMbMeis88CXBMYlk,7374
|
|
||||||
setuptools/command/setopt.py,sha256=xZF2RCc4ABvE9eHHAzF50-fkQg3au8fcRUVVGd58k3U,5100
|
|
||||||
setuptools/command/test.py,sha256=k7xcq7D7bEehgxarbw-dW3AtmGZORqz8HjKR6FGJ3jk,1343
|
|
||||||
setuptools/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/compat/py310.py,sha256=8sqwWczIcrkzeAbhaim4pKVd4tXZdcqmebgdvzji0rc,141
|
|
||||||
setuptools/compat/py311.py,sha256=e6tJAFwZEP82hmMBl10HYeSypelo_Ti2wTjKZVKLwOE,790
|
|
||||||
setuptools/compat/py312.py,sha256=vYKVtdrdOTsO_R90dJkEXsFwfMJFuIFJflhIgHrjJ-Y,366
|
|
||||||
setuptools/compat/py39.py,sha256=BJMtnkfcqyTfccqjYQxfoRtU2nTnWaEESBVkshTiXqY,493
|
|
||||||
setuptools/config/NOTICE,sha256=Ld3wiBgpejuJ1D2V_2WdjahXQRCMkTbfo6TYVsBiO9g,493
|
|
||||||
setuptools/config/__init__.py,sha256=aiPnL9BJn1O6MfmuNXyn8W2Lp8u9qizRVqwPiOdPIjY,1499
|
|
||||||
setuptools/config/_apply_pyprojecttoml.py,sha256=SUyTw7A2btZ1lBuWKN5o42-Diyv95eGTiYJ3rZOnGSc,19120
|
|
||||||
setuptools/config/distutils.schema.json,sha256=Tcp32kRnhwORGw_9p6GEi08lj2h15tQRzOYBbzGmcBU,972
|
|
||||||
setuptools/config/expand.py,sha256=JNAktRCsyyRB-rQodbPnCucmLWqcYvzCDC8Ebn2Z7xM,16041
|
|
||||||
setuptools/config/pyprojecttoml.py,sha256=YMu5PdbJJI5azp6kR_boM1mflf5nqOA-InF4s6LnLgw,18320
|
|
||||||
setuptools/config/setupcfg.py,sha256=VZDkwE7DYv45SbadJD8CwKrDtiXvjgllL8PYSvoRCyg,26575
|
|
||||||
setuptools/config/setuptools.schema.json,sha256=dZBRuSEnZkatoVlt1kVwG8ocTeRdO7BD0xvOWKH54PY,16071
|
|
||||||
setuptools/config/_validate_pyproject/NOTICE,sha256=XTANv6ZDE4sBO3WsnK7uWR-VG4sO4kKIw0zNkmxHgMg,18737
|
|
||||||
setuptools/config/_validate_pyproject/__init__.py,sha256=dnp6T7ePP1R5z4OuC7Fd2dkFlIrtIfizUfvpGJP6nz0,1042
|
|
||||||
setuptools/config/_validate_pyproject/error_reporting.py,sha256=meldD7nBQdolQhvG-43r1Ue-gU1n7ORAJR86vh3Rrvk,11813
|
|
||||||
setuptools/config/_validate_pyproject/extra_validations.py,sha256=-GUG5S--ijY8WfXbdXPoHl6ywGsyEF9dtDpenSoJPHg,2858
|
|
||||||
setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py,sha256=w749JgqKi8clBFcObdcbZVqsmF4oJ_QByhZ1SGbUFNw,1612
|
|
||||||
setuptools/config/_validate_pyproject/fastjsonschema_validations.py,sha256=FihD5ZcM6p77BPZ04CGqh3BEwVNoPMKJZJAyuJpkAU0,354682
|
|
||||||
setuptools/config/_validate_pyproject/formats.py,sha256=TETokJBK9hjl-cVg1olsojkJwLxfP7_chgJQNmzAB98,13564
|
|
||||||
setuptools/tests/__init__.py,sha256=AnBfls2iJbTDQzmMKeLRt-9lxhaOHUVOZEgXv89Uwvs,335
|
|
||||||
setuptools/tests/contexts.py,sha256=TAdZKxmmodx1ExMVo01o4QpRjpIpo4X3IWKq_BnjxpU,3480
|
|
||||||
setuptools/tests/environment.py,sha256=95_UtTaRiuvwYC9eXKEHbn02kDtZysvZq3UZJmPUj1I,3102
|
|
||||||
setuptools/tests/fixtures.py,sha256=-V7iD6BeE2E0Rw6dVvTOCm36JG8ZTTnrXhN0GISlgrg,5197
|
|
||||||
setuptools/tests/mod_with_constant.py,sha256=X_Kj80M55w1tmQ4f7uZY91ZTALo4hKVT6EHxgYocUMQ,22
|
|
||||||
setuptools/tests/namespaces.py,sha256=HPcI3nR5MCFWXpaADIJ1fwKxymcQgBkuw87Ic5PUSAQ,2774
|
|
||||||
setuptools/tests/script-with-bom.py,sha256=hRRgIizEULGiG_ZTNoMY46HhKhxpWfy5FGcD6Qbh5fc,18
|
|
||||||
setuptools/tests/server.py,sha256=0FDZf0cSInCP5n1haK7AxHo3jD261RK7D3-RjP-F53k,2397
|
|
||||||
setuptools/tests/test_archive_util.py,sha256=buuKdY8XkW26Pe3IKAoBRGHG0MDumnuNoPg2WsAQzIg,845
|
|
||||||
setuptools/tests/test_bdist_deprecations.py,sha256=75Xq3gYn79LIIyusEltbHan0bEgAt2e_CaL7KLS8-KQ,775
|
|
||||||
setuptools/tests/test_bdist_egg.py,sha256=6PaYN1F3JDbIh1uK0urv7yJFcx98z5dn9SOJ8Mv91l8,1957
|
|
||||||
setuptools/tests/test_bdist_wheel.py,sha256=dZ9a7OT_UyRvLnoCi2KGEIbtzhEQjM3YutYMA6ZCezs,23083
|
|
||||||
setuptools/tests/test_build.py,sha256=wJgMz2hwHADcLFg-nXrwRVhus7hjmAeEGgrpIQwCGnA,798
|
|
||||||
setuptools/tests/test_build_clib.py,sha256=bX51XRAf4uO7IuHFpjePnoK8mE74N2gsoeEqF-ofgws,3123
|
|
||||||
setuptools/tests/test_build_ext.py,sha256=e4ZSxsYPB5zq1KSqGEuATZ0t0PJQzMhjjkKJ-hIjcgc,10099
|
|
||||||
setuptools/tests/test_build_meta.py,sha256=kvi0Bn4p9DBVme3zyWQsn3QgB9oPdq8S15agj1m69L0,33289
|
|
||||||
setuptools/tests/test_build_py.py,sha256=gobME_Cvzf6Ugxq70iWfXekb_xyyT61khwjFq8zkwfw,14186
|
|
||||||
setuptools/tests/test_config_discovery.py,sha256=FqV-lOtkqaI-ayzU2zocSdD5TaRAgCZnixNDilKA6FQ,22580
|
|
||||||
setuptools/tests/test_core_metadata.py,sha256=vbVJ5_Lsx_hsO_GdB6nQEXJRjA2ydx6_qSbr5LpheAA,20881
|
|
||||||
setuptools/tests/test_depends.py,sha256=yQBXoQbNQlJit6mbRVoz6Bb553f3sNrq02lZimNz5XY,424
|
|
||||||
setuptools/tests/test_develop.py,sha256=CLzXZ8-b5-VFTuau4P4yXEdLx1UdyTFcOfrV0qyUIdE,5142
|
|
||||||
setuptools/tests/test_dist.py,sha256=GFjyL2etAxvVM3q7NhFEGcXS5gyKj8VzbqcbKzpqbOk,8901
|
|
||||||
setuptools/tests/test_dist_info.py,sha256=5kBRj9tuBsVreBsY22H2feMO_JQZsSoOZMU_MJfUevY,7077
|
|
||||||
setuptools/tests/test_distutils_adoption.py,sha256=_eynrOfyEqXFEmjUJhzpe8GXPyTUPvNSObs4qAAmBy8,5987
|
|
||||||
setuptools/tests/test_easy_install.py,sha256=jx4lpFyee0G432cdnwBow3AkL4ibw-0QILwldwv5SCI,53534
|
|
||||||
setuptools/tests/test_editable_install.py,sha256=7eTEtpT0k7QeVyZg64eh3kZn-SjckuB9LcokOuV37DI,43383
|
|
||||||
setuptools/tests/test_egg_info.py,sha256=QCzoUOkFocmbkwS6XU7F8WNzKE8CGEMRxYBqKLgfUrc,44866
|
|
||||||
setuptools/tests/test_extern.py,sha256=rpKU6oCcksumLwf5TeKlDluFQ0TUfbPwTLQbpxcFrCU,296
|
|
||||||
setuptools/tests/test_find_packages.py,sha256=CTLAcTzWGWBLCcd2aAsUVkvO3ibrlqexFBdDKOWPoq8,7819
|
|
||||||
setuptools/tests/test_find_py_modules.py,sha256=zQjuhIG5TQN2SJPix9ARo4DL_w84Ln8QsHDUjjbrtAQ,2404
|
|
||||||
setuptools/tests/test_glob.py,sha256=P3JvpH-kXQ4BZ3zvRF-zKxOgwyWzwIaQIz0WHdxS0kk,887
|
|
||||||
setuptools/tests/test_install_scripts.py,sha256=scIrJ6a_ssKqg4vIBNaUjmAKHEYLUUZ9WKnPeKnE6gc,3433
|
|
||||||
setuptools/tests/test_logging.py,sha256=zlE5DlldukC7Jc54FNvDV_7ux3ErAkrfrN5CSsnNOUQ,2099
|
|
||||||
setuptools/tests/test_manifest.py,sha256=eMg65pIA52DizB6mpktSU-b8CjwaNCS5MSgL_V1LrFI,18562
|
|
||||||
setuptools/tests/test_namespaces.py,sha256=Y6utoe5PHHqL_DlgawqB9F8XpsUDPvvw1sQMenK04e0,4515
|
|
||||||
setuptools/tests/test_packageindex.py,sha256=qEjLHpSu2gAkegwEstzHQT-Om1uQIYjA8zeNzEX79uo,8775
|
|
||||||
setuptools/tests/test_sandbox.py,sha256=shUWE7fLTWe7Jzdfi6NheBWauG3oUukbUV46cArD0u0,4330
|
|
||||||
setuptools/tests/test_sdist.py,sha256=RYLvPa_nfyC1ZmoinzqMzJynTDG4RtPYC19_0LU6pvs,32872
|
|
||||||
setuptools/tests/test_setopt.py,sha256=3VxxM4ATfP-P4AGnDjoWCnHr5-i9CSEQTFYU1-FTnvI,1365
|
|
||||||
setuptools/tests/test_setuptools.py,sha256=_eIhqKf45-OtHqxRf20KndOZJlJdS0PuFLXBO3M-LN8,9008
|
|
||||||
setuptools/tests/test_shutil_wrapper.py,sha256=g15E11PtZxG-InB2BWNFyH-svObXx2XcMhgMLJPuFnc,641
|
|
||||||
setuptools/tests/test_unicode_utils.py,sha256=xWfEEl8jkQCt9othUTXJfFmdyATAFggJs2tTxjbumbw,316
|
|
||||||
setuptools/tests/test_virtualenv.py,sha256=g-njC_9JTAs1YVx_1dGJ_Q6RlInO4qKVu9-XAgNb6TY,3730
|
|
||||||
setuptools/tests/test_warnings.py,sha256=zwR2zcnCeCeDqILZlJOPAcuyPHoDvGu1OtOVYiLMk74,3347
|
|
||||||
setuptools/tests/test_wheel.py,sha256=J-83W1KdXTgAjFZE3H-ytohhvDE1iqdbE5YF5jLQlGQ,19370
|
|
||||||
setuptools/tests/test_windows_wrappers.py,sha256=aF6UTowN3yzCgdBh9nDQVvYIfSYogrTK776TEyXEBqg,7881
|
|
||||||
setuptools/tests/text.py,sha256=a12197pMVTvB6FAWQ0ujT8fIQiLIWJlFAl1UCaDUDfg,123
|
|
||||||
setuptools/tests/textwrap.py,sha256=FNNNq_MiaEJx88PnsbJQIRxmj1qmgcAOCXXRsODPJN4,98
|
|
||||||
setuptools/tests/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/tests/compat/py39.py,sha256=eUy7_F-6KRTOIKl-veshUu6I0EdTSdBZMh0EV0lZ1-g,135
|
|
||||||
setuptools/tests/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/tests/config/setupcfg_examples.txt,sha256=cAbVvCbkFZuTUL6xRRzRgqyB0rLvJTfvw3D30glo2OE,1912
|
|
||||||
setuptools/tests/config/test_apply_pyprojecttoml.py,sha256=l6nE4d8WLU_eSWRic7VSoqeKv9Bi7CZGHcEuB2ehk2w,28807
|
|
||||||
setuptools/tests/config/test_expand.py,sha256=S0oT6JvgA_oujR4YS4RUuf5gmOt1CTQV66RQDzV8xd4,8933
|
|
||||||
setuptools/tests/config/test_pyprojecttoml.py,sha256=0LefSljUhA6MqtJ5AVzLhomqZcYiFKdu_1ckDeMT1LY,12406
|
|
||||||
setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py,sha256=9W73-yLhZJmvCiO4rTiQoBpZT5wNA90Xbd5n2HCshd4,3271
|
|
||||||
setuptools/tests/config/test_setupcfg.py,sha256=ZvN-O-2Dgon1adp6oM6il8JWdgT9y196fRvqESU5ELI,33427
|
|
||||||
setuptools/tests/config/downloads/__init__.py,sha256=9ixnDEdyL_arKbUzfuiJftAj9bGxKz8M9alOFZMjx9Y,1827
|
|
||||||
setuptools/tests/config/downloads/preload.py,sha256=sIGGZpY3cmMpMwiJYYYYHG2ifZJkvJgEotRFtiulV1I,450
|
|
||||||
setuptools/tests/indexes/test_links_priority/external.html,sha256=eL9euOuE93JKZdqlXxBOlHbKwIuNuIdq7GBRpsaPMcU,92
|
|
||||||
setuptools/tests/indexes/test_links_priority/simple/foobar/index.html,sha256=DD-TKr7UU4zAjHHz4VexYDNSAzR27levSh1c-k3ZdLE,174
|
|
||||||
setuptools/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/tests/integration/helpers.py,sha256=3PHcS9SCA-fwVJmUP2ad5NQOttJAETI5Nnoc_xroO5k,2522
|
|
||||||
setuptools/tests/integration/test_pip_install_sdist.py,sha256=SFbvuYF_hDzt6OtsQ5GjFNnxmoJ_eElfvpYsiyyGJ-g,8256
|
|
||||||
setuptools-79.0.1.dist-info/licenses/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
|
||||||
setuptools-79.0.1.dist-info/METADATA,sha256=0xUsrrFIbRQ7MLHUH_-q3bfqlf3JVHrhE1zml7GkILU,6548
|
|
||||||
setuptools-79.0.1.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
||||||
setuptools-79.0.1.dist-info/entry_points.txt,sha256=zkgthpf_Fa9NVE9p6FKT3Xk9DR1faAcRU4coggsV7jA,2449
|
|
||||||
setuptools-79.0.1.dist-info/top_level.txt,sha256=d9yL39v_W7qmKDDSH6sT4bE0j_Ls1M3P161OGgdsm4g,41
|
|
||||||
setuptools/_vendor/autocommand-2.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/autocommand-2.2.2.dist-info/LICENSE,sha256=reeNBJgtaZctREqOFKlPh6IzTdOFXMgDSOqOJAqg3y0,7634
|
|
||||||
setuptools/_vendor/autocommand-2.2.2.dist-info/METADATA,sha256=OADZuR3O6iBlpu1ieTgzYul6w4uOVrk0P0BO5TGGAJk,15006
|
|
||||||
setuptools/_vendor/autocommand-2.2.2.dist-info/RECORD,sha256=giu6ZrQVJvpUcYa4AiH4XaUNZSvuVJPb_l0UCFES8MM,1308
|
|
||||||
setuptools/_vendor/autocommand-2.2.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
|
||||||
setuptools/_vendor/autocommand-2.2.2.dist-info/top_level.txt,sha256=AzfhgKKS8EdAwWUTSF8mgeVQbXOY9kokHB6kSqwwqu0,12
|
|
||||||
setuptools/_vendor/backports.tarfile-1.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/backports.tarfile-1.2.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
|
||||||
setuptools/_vendor/backports.tarfile-1.2.0.dist-info/METADATA,sha256=ghXFTq132dxaEIolxr3HK1mZqm9iyUmaRANZQSr6WlE,2020
|
|
||||||
setuptools/_vendor/backports.tarfile-1.2.0.dist-info/RECORD,sha256=JYofHISeEXUGmlWl1s41ev3QTjTNXeJwk-Ss7HqdLOE,1360
|
|
||||||
setuptools/_vendor/backports.tarfile-1.2.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/backports.tarfile-1.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
||||||
setuptools/_vendor/backports.tarfile-1.2.0.dist-info/top_level.txt,sha256=cGjaLMOoBR1FK0ApojtzWVmViTtJ7JGIK_HwXiEsvtU,10
|
|
||||||
setuptools/_vendor/importlib_metadata-8.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/importlib_metadata-8.0.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
||||||
setuptools/_vendor/importlib_metadata-8.0.0.dist-info/METADATA,sha256=anuQ7_7h4J1bSEzfcjIBakPi2cyVQ7y7jklLHsBeH1k,4648
|
|
||||||
setuptools/_vendor/importlib_metadata-8.0.0.dist-info/RECORD,sha256=DY08buueu-hsrH1ghhVSQzwynanqUSSLYdAr4uXmQDA,2518
|
|
||||||
setuptools/_vendor/importlib_metadata-8.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/importlib_metadata-8.0.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
|
|
||||||
setuptools/_vendor/importlib_metadata-8.0.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
|
|
||||||
setuptools/_vendor/inflect-7.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/inflect-7.3.1.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
|
||||||
setuptools/_vendor/inflect-7.3.1.dist-info/METADATA,sha256=ZgMNY0WAZRs-U8wZiV2SMfjSKqBrMngXyDMs_CAwMwg,21079
|
|
||||||
setuptools/_vendor/inflect-7.3.1.dist-info/RECORD,sha256=XXg0rBuiYSxoAQUP3lenuYsPNqz4jDwtTzdv2JEbMJE,943
|
|
||||||
setuptools/_vendor/inflect-7.3.1.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
|
||||||
setuptools/_vendor/inflect-7.3.1.dist-info/top_level.txt,sha256=m52ujdp10CqT6jh1XQxZT6kEntcnv-7Tl7UiGNTzWZA,8
|
|
||||||
setuptools/_vendor/jaraco.collections-5.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/jaraco.collections-5.1.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
|
||||||
setuptools/_vendor/jaraco.collections-5.1.0.dist-info/METADATA,sha256=IMUaliNsA5X1Ox9MXUWOagch5R4Wwb_3M7erp29dBtg,3933
|
|
||||||
setuptools/_vendor/jaraco.collections-5.1.0.dist-info/RECORD,sha256=HptivXDkpfom6VlMu4CGD_7KPev-6Hc9rvp3TNJZygY,873
|
|
||||||
setuptools/_vendor/jaraco.collections-5.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/jaraco.collections-5.1.0.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
|
|
||||||
setuptools/_vendor/jaraco.collections-5.1.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7
|
|
||||||
setuptools/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/jaraco.context-5.3.0.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
|
||||||
setuptools/_vendor/jaraco.context-5.3.0.dist-info/METADATA,sha256=xDtguJej0tN9iEXCUvxEJh2a7xceIRVBEakBLSr__tY,4020
|
|
||||||
setuptools/_vendor/jaraco.context-5.3.0.dist-info/RECORD,sha256=VRl7iKeEQyl7stgnp1uq50CzOJYlHYcoNdS0x17C9X4,641
|
|
||||||
setuptools/_vendor/jaraco.context-5.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
||||||
setuptools/_vendor/jaraco.context-5.3.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7
|
|
||||||
setuptools/_vendor/jaraco.functools-4.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/jaraco.functools-4.0.1.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
|
||||||
setuptools/_vendor/jaraco.functools-4.0.1.dist-info/METADATA,sha256=i4aUaQDX-jjdEQK5wevhegyx8JyLfin2HyvaSk3FHso,2891
|
|
||||||
setuptools/_vendor/jaraco.functools-4.0.1.dist-info/RECORD,sha256=YyqnwE98S8wBwCevW5vHb-iVj0oYEDW5V6O9MBS6JIs,843
|
|
||||||
setuptools/_vendor/jaraco.functools-4.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
||||||
setuptools/_vendor/jaraco.functools-4.0.1.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7
|
|
||||||
setuptools/_vendor/jaraco.text-3.12.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/jaraco.text-3.12.1.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
|
||||||
setuptools/_vendor/jaraco.text-3.12.1.dist-info/METADATA,sha256=AzWdm6ViMfDOPoQMfLWn2zgBQSGJScyqeN29TcuWXVI,3658
|
|
||||||
setuptools/_vendor/jaraco.text-3.12.1.dist-info/RECORD,sha256=gW2UV0HcokYJk4jKPu10_AZnrLqjb3C1WbJJTDl5sfY,1500
|
|
||||||
setuptools/_vendor/jaraco.text-3.12.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/jaraco.text-3.12.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
||||||
setuptools/_vendor/jaraco.text-3.12.1.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7
|
|
||||||
setuptools/_vendor/more_itertools-10.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/more_itertools-10.3.0.dist-info/LICENSE,sha256=CfHIyelBrz5YTVlkHqm4fYPAyw_QB-te85Gn4mQ8GkY,1053
|
|
||||||
setuptools/_vendor/more_itertools-10.3.0.dist-info/METADATA,sha256=BFO90O-fLNiVQMpj7oIS5ztzgJUUQZ3TA32P5HH3N-A,36293
|
|
||||||
setuptools/_vendor/more_itertools-10.3.0.dist-info/RECORD,sha256=d8jnPgGNwP1-ntbICwWkQEVF9kH7CFIgzkKzaLWao9M,1259
|
|
||||||
setuptools/_vendor/more_itertools-10.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/more_itertools-10.3.0.dist-info/WHEEL,sha256=rSgq_JpHF9fHR1lx53qwg_1-2LypZE_qmcuXbVUq948,81
|
|
||||||
setuptools/_vendor/packaging-24.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
|
||||||
setuptools/_vendor/packaging-24.2.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
|
|
||||||
setuptools/_vendor/packaging-24.2.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
|
||||||
setuptools/_vendor/packaging-24.2.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
|
|
||||||
setuptools/_vendor/packaging-24.2.dist-info/METADATA,sha256=ohH86s6k5mIfQxY2TS0LcSfADeOFa4BiCC-bxZV-pNs,3204
|
|
||||||
setuptools/_vendor/packaging-24.2.dist-info/RECORD,sha256=Y4DrXM0KY0ArfzhbAEa1LYFPwW3WEgEeL4iCqXe-A-M,2009
|
|
||||||
setuptools/_vendor/packaging-24.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/packaging-24.2.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
|
||||||
setuptools/_vendor/platformdirs-4.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/platformdirs-4.2.2.dist-info/METADATA,sha256=zmsie01G1MtXR0wgIv5XpVeTO7idr0WWvfmxKsKWuGk,11429
|
|
||||||
setuptools/_vendor/platformdirs-4.2.2.dist-info/RECORD,sha256=TCEddtQu1A78Os_Mhm2JEqcYr7yit-UYSUQjZtbpn-g,1642
|
|
||||||
setuptools/_vendor/platformdirs-4.2.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/platformdirs-4.2.2.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
|
||||||
setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE,sha256=uAgWsNUwuKzLTCIReDeQmEpuO2GSLCte6S8zcqsnQv4,1072
|
|
||||||
setuptools/_vendor/tomli-2.0.1.dist-info/METADATA,sha256=zPDceKmPwJGLWtZykrHixL7WVXWmJGzZ1jyRT5lCoPI,8875
|
|
||||||
setuptools/_vendor/tomli-2.0.1.dist-info/RECORD,sha256=DLn5pFGh42WsVLTIhmLh2gy1SnLRalJY-wq_-dPhwCI,999
|
|
||||||
setuptools/_vendor/tomli-2.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/tomli-2.0.1.dist-info/WHEEL,sha256=jPMR_Dzkc4X4icQtmz81lnNY_kAsfog7ry7qoRvYLXw,81
|
|
||||||
setuptools/_vendor/typeguard-4.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/typeguard-4.3.0.dist-info/LICENSE,sha256=YWP3mH37ONa8MgzitwsvArhivEESZRbVUu8c1DJH51g,1130
|
|
||||||
setuptools/_vendor/typeguard-4.3.0.dist-info/METADATA,sha256=z2dcHAp0TwhYCFU5Deh8x31nazElgujUz9tbuP0pjSE,3717
|
|
||||||
setuptools/_vendor/typeguard-4.3.0.dist-info/RECORD,sha256=SKUZWVgkeDUidUKM7s1473fXmsna55bjmi6vJUAoJVI,2402
|
|
||||||
setuptools/_vendor/typeguard-4.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
||||||
setuptools/_vendor/typeguard-4.3.0.dist-info/entry_points.txt,sha256=qp7NQ1aLtiSgMQqo6gWlfGpy0IIXzoMJmeQTLpzqFZQ,48
|
|
||||||
setuptools/_vendor/typeguard-4.3.0.dist-info/top_level.txt,sha256=4z28AhuDodwRS_c1J_l8H51t5QuwfTseskYzlxp6grs,10
|
|
||||||
setuptools/_vendor/typing_extensions-4.12.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/typing_extensions-4.12.2.dist-info/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936
|
|
||||||
setuptools/_vendor/typing_extensions-4.12.2.dist-info/METADATA,sha256=BeUQIa8cnYbrjWx-N8TOznM9UGW5Gm2DicVpDtRA8W0,3018
|
|
||||||
setuptools/_vendor/typing_extensions-4.12.2.dist-info/RECORD,sha256=dxAALYGXHmMqpqL8M9xddKr118quIgQKZdPjFQOwXuk,571
|
|
||||||
setuptools/_vendor/typing_extensions-4.12.2.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
|
||||||
setuptools/_vendor/wheel-0.45.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107
|
|
||||||
setuptools/_vendor/wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313
|
|
||||||
setuptools/_vendor/wheel-0.45.1.dist-info/RECORD,sha256=1jnxrHyZPDcVvULyfGFhiba4Z5L9_RsXr9dxcNbhaYQ,4900
|
|
||||||
setuptools/_vendor/wheel-0.45.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/wheel-0.45.1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
|
||||||
setuptools/_vendor/wheel-0.45.1.dist-info/entry_points.txt,sha256=rTY1BbkPHhkGMm4Q3F0pIzJBzW2kMxoG1oriffvGdA0,104
|
|
||||||
setuptools/_vendor/zipp-3.19.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
setuptools/_vendor/zipp-3.19.2.dist-info/LICENSE,sha256=htoPAa6uRjSKPD1GUZXcHOzN55956HdppkuNoEsqR0E,1023
|
|
||||||
setuptools/_vendor/zipp-3.19.2.dist-info/METADATA,sha256=UIrk_kMIHGSwsKKChYizqMw0MMZpPRZ2ZiVpQAsN_bE,3575
|
|
||||||
setuptools/_vendor/zipp-3.19.2.dist-info/RECORD,sha256=8xby4D_ZrefrvAsVRwaEjiu4_VaLkJNRCfDY484rm_4,1039
|
|
||||||
setuptools/_vendor/zipp-3.19.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
setuptools/_vendor/zipp-3.19.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
||||||
setuptools/_vendor/zipp-3.19.2.dist-info/top_level.txt,sha256=iAbdoSHfaGqBfVb2XuR9JqSQHCoOsOtG6y9C_LSpqFw,5
|
|
||||||
setuptools-79.0.1.dist-info/RECORD,,
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: setuptools (79.0.1)
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
||||||
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
[distutils.commands]
|
|
||||||
alias = setuptools.command.alias:alias
|
|
||||||
bdist_egg = setuptools.command.bdist_egg:bdist_egg
|
|
||||||
bdist_rpm = setuptools.command.bdist_rpm:bdist_rpm
|
|
||||||
bdist_wheel = setuptools.command.bdist_wheel:bdist_wheel
|
|
||||||
build = setuptools.command.build:build
|
|
||||||
build_clib = setuptools.command.build_clib:build_clib
|
|
||||||
build_ext = setuptools.command.build_ext:build_ext
|
|
||||||
build_py = setuptools.command.build_py:build_py
|
|
||||||
develop = setuptools.command.develop:develop
|
|
||||||
dist_info = setuptools.command.dist_info:dist_info
|
|
||||||
easy_install = setuptools.command.easy_install:easy_install
|
|
||||||
editable_wheel = setuptools.command.editable_wheel:editable_wheel
|
|
||||||
egg_info = setuptools.command.egg_info:egg_info
|
|
||||||
install = setuptools.command.install:install
|
|
||||||
install_egg_info = setuptools.command.install_egg_info:install_egg_info
|
|
||||||
install_lib = setuptools.command.install_lib:install_lib
|
|
||||||
install_scripts = setuptools.command.install_scripts:install_scripts
|
|
||||||
rotate = setuptools.command.rotate:rotate
|
|
||||||
saveopts = setuptools.command.saveopts:saveopts
|
|
||||||
sdist = setuptools.command.sdist:sdist
|
|
||||||
setopt = setuptools.command.setopt:setopt
|
|
||||||
|
|
||||||
[distutils.setup_keywords]
|
|
||||||
dependency_links = setuptools.dist:assert_string_list
|
|
||||||
eager_resources = setuptools.dist:assert_string_list
|
|
||||||
entry_points = setuptools.dist:check_entry_points
|
|
||||||
exclude_package_data = setuptools.dist:check_package_data
|
|
||||||
extras_require = setuptools.dist:check_extras
|
|
||||||
include_package_data = setuptools.dist:assert_bool
|
|
||||||
install_requires = setuptools.dist:check_requirements
|
|
||||||
namespace_packages = setuptools.dist:check_nsp
|
|
||||||
package_data = setuptools.dist:check_package_data
|
|
||||||
packages = setuptools.dist:check_packages
|
|
||||||
python_requires = setuptools.dist:check_specifier
|
|
||||||
setup_requires = setuptools.dist:check_requirements
|
|
||||||
use_2to3 = setuptools.dist:invalid_unless_false
|
|
||||||
zip_safe = setuptools.dist:assert_bool
|
|
||||||
|
|
||||||
[egg_info.writers]
|
|
||||||
PKG-INFO = setuptools.command.egg_info:write_pkg_info
|
|
||||||
dependency_links.txt = setuptools.command.egg_info:overwrite_arg
|
|
||||||
eager_resources.txt = setuptools.command.egg_info:overwrite_arg
|
|
||||||
entry_points.txt = setuptools.command.egg_info:write_entries
|
|
||||||
namespace_packages.txt = setuptools.command.egg_info:overwrite_arg
|
|
||||||
requires.txt = setuptools.command.egg_info:write_requirements
|
|
||||||
top_level.txt = setuptools.command.egg_info:write_toplevel_names
|
|
||||||
|
|
||||||
[setuptools.finalize_distribution_options]
|
|
||||||
keywords = setuptools.dist:Distribution._finalize_setup_keywords
|
|
||||||
parent_finalize = setuptools.dist:_Distribution.finalize_options
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
_distutils_hack
|
|
||||||
pkg_resources
|
|
||||||
setuptools
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
"""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()
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
log = logging.getLogger()
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,554 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
"""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',
|
|
||||||
]
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""
|
|
||||||
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'
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,598 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
"""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),
|
|
||||||
]
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
"""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
|
|
||||||
)
|
|
||||||
@@ -1,812 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
"""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,
|
|
||||||
)
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
"""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})"
|
|
||||||
)
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
"""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'))
|
|
||||||
@@ -1,805 +0,0 @@
|
|||||||
"""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),
|
|
||||||
]
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
"""
|
|
||||||
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('-', '_')
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
"""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 []
|
|
||||||
@@ -1,521 +0,0 @@
|
|||||||
"""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('#')
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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)
|
|
||||||
)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,340 +0,0 @@
|
|||||||
"""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.
|
|
||||||
"""
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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."""
|
|
||||||
@@ -1,614 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
"""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,
|
|
||||||
)
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
"""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 ()
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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.
|
|
||||||
"""
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
# If DISTUTILS_DEBUG is anything other than the empty string, we run in
|
|
||||||
# debug mode.
|
|
||||||
DEBUG = os.environ.get('DISTUTILS_DEBUG')
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
"""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
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,108 +0,0 @@
|
|||||||
"""
|
|
||||||
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."""
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,471 +0,0 @@
|
|||||||
"""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()
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,598 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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')
|
|
||||||
@@ -1,518 +0,0 @@
|
|||||||
"""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'))
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
#
|
|
||||||
# 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
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .compilers.C import zos
|
|
||||||
|
|
||||||
zOSCCompiler = zos.Compiler
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
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))
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
"""
|
|
||||||
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)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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))
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pip
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
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.
|
|
||||||
|
|
||||||
@@ -1,420 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: bdist_wheel (0.38.4)
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
autocommand
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pip
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: bdist_wheel (0.43.0)
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
backports
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user